Skip to content

Commit

Permalink
Merge pull request #88 from marick/master
Browse files Browse the repository at this point in the history
Step 1 toward Prism documentation: a "how to" module for Prisms.
  • Loading branch information
marick committed May 7, 2018
2 parents 569d819 + 4507fd8 commit 02b1693
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ bower install purescript-profunctor-lenses

Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-profunctor-lenses).

You can find an example usage [here](test/Main.purs).
You can find examples in the [tests](test/Main.purs) and the [examples](examples/README.md) directory.

8 changes: 8 additions & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/bower_components/
/node_modules/
/.pulp-cache/
/output/
/generated-docs/
/.psc*
/.purs*
/.psa*
53 changes: 53 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## Examples

* [Using Prisms with Sum Types](src/PrismsForSumTypes.purs)

## Conventions and the "why" behind them

The examples are meant to be read from top to bottom.

Every example is self-contained and can be imported into the repl. The
top of an example module will have a comment like this:

```purescript
{- If you want to try out examples, paste the following into the repl.
import PrismsForSumTypes
import Data.Lens
...
-}
```

(Tip: because each line is self-contained, you don't need to use `:paste`.)

The modules contain both definitions of optics and sample uses. The
sample uses are executable code that look like this:

```purescript
s1 :: Maybe Color
s1 = preview solidFocus (Solid Color.white)
-- (Just rgba 255 255 255 1.0)
```

Why?

1. The second line can be pasted into the repl without needing to use
`:paste`. (I usually don't copy the `s1 =` because I prefer seeing
the results immediately. All types in the examples implement `show`,
making that painless.)

2. The sample uses are executable code so that the compiler checks
them and -- more importantly -- so that they stand out from the
surrounding commentary. That makes the files more easily scannable
when you're refreshing your memory about a particular function
(assuming you use a syntax highlighter).

3. The name-value bindings (like `s1`) are clutter, but required by the
compiler. Most of the type annotations are also clutter, but are required
to prevent compiler warnings in, for example, `pulp build`.

4. The expected value is on a new line, rather than appended to the
end of the second line, because it's visually easier to scan down a line
than scan right for a `--` pattern.

"Comfort is the key." -- Prof. Dawn Marick, DVM, MS, DACVIM (LAIM)
21 changes: 21 additions & 0 deletions examples/bower.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "examples",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"output"
],
"dependencies": {
"purescript-prelude": "^3.3.0",
"purescript-console": "^3.0.0",
"purescript-profunctor-lenses": "^3.8.0",
"purescript-record-show": "^0.4.0",
"purescript-colors": "^4.3.0",
"purescript-generics-rep": "^5.4.0",
"purescript-numbers": "^5.0.0"
},
"devDependencies": {
"purescript-psci-support": "^3.0.0"
}
}
255 changes: 255 additions & 0 deletions examples/src/PrismsForSumTypes.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
module PrismsForSumTypes where

{- Prisms are optics that "focus" on one case of a sum type. They can
also be used for other kinds of case analysis, but sum types are most
common. This introduction only discusses sum types.
Use a Prism if you want to write code like this:
preview prismForSolidFill $ Solid Color.white
-- Just Color.white
preview prismForSolidFill NoFill
-- Nothing
review prismForSolidFill Color.white
-- Solid Color.white
-}

{- If you want to try out examples, paste the following into the repl.
import PrismsForSumTypes
import Data.Lens
import Data.Lens.Prism
import Color as Color
import Data.Maybe
import Data.Record.ShowRecord (showRecord)
See `README.md` if you're wondering why code is formatted the way it is.
-}

import Prelude
import Data.Lens (Prism', is, isn't, nearly, only, preview, prism, prism', review)

import Color (Color)
import Color as Color

import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Eq as GEq
import Data.Generic.Rep.Show as GShow
import Data.Record.ShowRecord (showRecord)
import Data.Maybe (Maybe(..), maybe)
import Data.Either (Either(..))


{- The types in question -}

newtype Percent = Percent Number
data Point = Point Number Number

data Fill -- think of a paint program filling a shape
= Solid Color
| LinearGradient Color Color Percent
| RadialGradient Color Color Point
| NoFill

{------ Some samples to work with ------}

fillBlackToWhite :: Fill
fillBlackToWhite = LinearGradient Color.black Color.white $ Percent 3.3

fillWhiteToBlack :: Fill
fillWhiteToBlack = LinearGradient Color.white Color.black $ Percent 3.3

fillRadial :: Fill
fillRadial = RadialGradient Color.white Color.black $ Point 1.0 3.4


{------ Making prisms with Maybe and `prism'` ------}

-- `prism'` (note the apostrophe) takes two functions. One is a data
-- constructor for the type in question. The other converts your
-- desired case to a `Just <wrapped values>` or `Nothing`.

solidFocus :: Prism' Fill Color
solidFocus = prism' constructor focus
where
constructor = Solid
focus fill = case fill of
Solid color -> Just color
otherCases -> Nothing

-- In real life, you might abbreviate the above to this:

solidFocus' :: Prism' Fill Color
solidFocus' = prism' Solid case _ of
Solid color -> Just color
_ -> Nothing

-- ... but being painfully explicit is better for examples.


{------ Basic usage: `preview`, `review`, `is`, and `isn't` ------}

-- After building a prism, you focus in on a color with `preview`:

s1 :: Maybe Color
s1 = preview solidFocus (Solid Color.white)
-- (Just rgba 255 255 255 1.0)

s2 :: Maybe Color
s2 = preview solidFocus fillRadial
-- Nothing

-- ... or you can create a Fill from a color with `review`:

s3 :: Fill
s3 = review solidFocus Color.white
-- (Solid rgba 255 255 255 1.0)

-- ... or you can ask whether a given value matches the prism:

s4 :: Boolean
s4 = is solidFocus (Solid Color.white) :: Boolean
-- true

s5 :: Boolean
s5 = isn't solidFocus (Solid Color.white) :: Boolean
-- false


{------ Making prisms with Either and `prism` ------}

-- Since `LinearGradient` wraps two colors and a percent, they need to
-- be bundled together into a single value for `preview` to
-- return. I'll use a record:

type LinearInterchange =
{ color1 :: Color
, color2 :: Color
, percent :: Percent
}

-- When making a prism with `prism` (no apostrophe), the "focus"
-- function returns either the selected value (as a `Right`) or the
-- entire argument (as a `Left`).

linearFocus :: Prism' Fill LinearInterchange
linearFocus = prism constructor focus
where
constructor {color1, color2, percent} =
LinearGradient color1 color2 percent
focus = case _ of
LinearGradient color1 color2 percent ->
Right {color1, color2, percent}
otherCases ->
Left otherCases

-- Even though made differently than `solidFocus`, `linearFocus` is
-- used the same way:

l1 :: String
l1 = preview linearFocus fillBlackToWhite # maybe "!" showRecord
-- "{ color1: rgba 0 0 0 1.0, color2: rgba 255 255 255 1.0, percent: (3.3%) }"

l2 :: Fill
l2 = review linearFocus { color1 : Color.black
, color2 : Color.white
, percent : Percent 33.3
}


{------ Use `only` to focus on specific values ------}

whiteToBlackFocus :: Prism' Fill Unit
whiteToBlackFocus = only fillWhiteToBlack

o1 :: Boolean
o1 = is whiteToBlackFocus fillWhiteToBlack :: Boolean
-- true

o2 :: Boolean
o2 = is whiteToBlackFocus fillBlackToWhite :: Boolean
-- false

o3 :: Boolean
o3 = is whiteToBlackFocus fillRadial :: Boolean
-- false

-- Note that `only` requires `Fill` to implement `Eq`.
-- It's the only prism constructor that does.

{------ Use `nearly` to focus on a sub-case ------}


-- `nearly` is typically used to look for a specific case (like other
-- prisms), but also accepts only values that are close to some target
-- value. It takes two values: a reference value, and a predicate that
-- determines whether the wrapped value(s) are close enough to the
-- reference. Note that the predicate takes the "whole" type (here,
-- `Fill`), not the unwrapped values inside the case you care about.

-- In this example, we want to focus on solid colors that are "bright
-- enough."

brightSolidFocus :: Prism' Fill Unit
brightSolidFocus = nearly (Solid referenceColor) predicate
where
referenceColor = Color.graytone 0.8
predicate = case _ of
Solid color ->
Color.brightness color >= Color.brightness referenceColor
_ ->
false

-- Because a `nearly` prism focuses into `Unit`, you can get only two
-- values from `preview`:

n1 :: Maybe Unit
n1 = preview brightSolidFocus (Solid Color.white)
-- (Just unit)

n2 :: Maybe Unit
n2 = preview brightSolidFocus (Solid Color.black)
-- Nothing

n3 :: Maybe Unit
n3 = preview brightSolidFocus NoFill
-- Nothing


-- ... so you probably want to use `is` or `isn't`:

n4 :: Boolean
n4 = is brightSolidFocus (Solid Color.white) :: Boolean
-- true

-- You can recover the reference value with `review`:

n5 :: Fill
n5 = review brightSolidFocus unit
-- (Solid rgba 204 204 204 1.0)



{------ Eq and Show are always nice ------}

-- ... although Eq is only required for `only`.

derive instance genericPercent :: Generic Percent _
instance eqPercent :: Eq Percent where
eq = GEq.genericEq
instance showPercent :: Show Percent where
show (Percent f) = "(" <> show f <> "%)"

derive instance genericPoint :: Generic Point _
instance eqPoint :: Eq Point where
eq = GEq.genericEq
instance showPoint :: Show Point where
show (Point x y) = "(" <> show x <> ", " <> show y <> ")"

derive instance genericFill :: Generic Fill _
instance eqFill :: Eq Fill where
eq = GEq.genericEq
instance showFill :: Show Fill where
show x = GShow.genericShow x

0 comments on commit 02b1693

Please sign in to comment.