Skip to content

Commit 02b1693

Browse files
authored
Merge pull request #88 from marick/master
Step 1 toward Prism documentation: a "how to" module for Prisms.
2 parents 569d819 + 4507fd8 commit 02b1693

File tree

5 files changed

+338
-1
lines changed

5 files changed

+338
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ bower install purescript-profunctor-lenses
1616

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

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

examples/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/bower_components/
2+
/node_modules/
3+
/.pulp-cache/
4+
/output/
5+
/generated-docs/
6+
/.psc*
7+
/.purs*
8+
/.psa*

examples/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
## Examples
2+
3+
* [Using Prisms with Sum Types](src/PrismsForSumTypes.purs)
4+
5+
## Conventions and the "why" behind them
6+
7+
The examples are meant to be read from top to bottom.
8+
9+
Every example is self-contained and can be imported into the repl. The
10+
top of an example module will have a comment like this:
11+
12+
```purescript
13+
{- If you want to try out examples, paste the following into the repl.
14+
15+
import PrismsForSumTypes
16+
import Data.Lens
17+
...
18+
-}
19+
```
20+
21+
(Tip: because each line is self-contained, you don't need to use `:paste`.)
22+
23+
The modules contain both definitions of optics and sample uses. The
24+
sample uses are executable code that look like this:
25+
26+
```purescript
27+
s1 :: Maybe Color
28+
s1 = preview solidFocus (Solid Color.white)
29+
-- (Just rgba 255 255 255 1.0)
30+
```
31+
32+
Why?
33+
34+
1. The second line can be pasted into the repl without needing to use
35+
`:paste`. (I usually don't copy the `s1 =` because I prefer seeing
36+
the results immediately. All types in the examples implement `show`,
37+
making that painless.)
38+
39+
2. The sample uses are executable code so that the compiler checks
40+
them and -- more importantly -- so that they stand out from the
41+
surrounding commentary. That makes the files more easily scannable
42+
when you're refreshing your memory about a particular function
43+
(assuming you use a syntax highlighter).
44+
45+
3. The name-value bindings (like `s1`) are clutter, but required by the
46+
compiler. Most of the type annotations are also clutter, but are required
47+
to prevent compiler warnings in, for example, `pulp build`.
48+
49+
4. The expected value is on a new line, rather than appended to the
50+
end of the second line, because it's visually easier to scan down a line
51+
than scan right for a `--` pattern.
52+
53+
"Comfort is the key." -- Prof. Dawn Marick, DVM, MS, DACVIM (LAIM)

examples/bower.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "examples",
3+
"ignore": [
4+
"**/.*",
5+
"node_modules",
6+
"bower_components",
7+
"output"
8+
],
9+
"dependencies": {
10+
"purescript-prelude": "^3.3.0",
11+
"purescript-console": "^3.0.0",
12+
"purescript-profunctor-lenses": "^3.8.0",
13+
"purescript-record-show": "^0.4.0",
14+
"purescript-colors": "^4.3.0",
15+
"purescript-generics-rep": "^5.4.0",
16+
"purescript-numbers": "^5.0.0"
17+
},
18+
"devDependencies": {
19+
"purescript-psci-support": "^3.0.0"
20+
}
21+
}

examples/src/PrismsForSumTypes.purs

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
module PrismsForSumTypes where
2+
3+
{- Prisms are optics that "focus" on one case of a sum type. They can
4+
also be used for other kinds of case analysis, but sum types are most
5+
common. This introduction only discusses sum types.
6+
7+
Use a Prism if you want to write code like this:
8+
9+
preview prismForSolidFill $ Solid Color.white
10+
-- Just Color.white
11+
12+
preview prismForSolidFill NoFill
13+
-- Nothing
14+
15+
review prismForSolidFill Color.white
16+
-- Solid Color.white
17+
-}
18+
19+
{- If you want to try out examples, paste the following into the repl.
20+
21+
import PrismsForSumTypes
22+
import Data.Lens
23+
import Data.Lens.Prism
24+
import Color as Color
25+
import Data.Maybe
26+
import Data.Record.ShowRecord (showRecord)
27+
28+
See `README.md` if you're wondering why code is formatted the way it is.
29+
-}
30+
31+
import Prelude
32+
import Data.Lens (Prism', is, isn't, nearly, only, preview, prism, prism', review)
33+
34+
import Color (Color)
35+
import Color as Color
36+
37+
import Data.Generic.Rep (class Generic)
38+
import Data.Generic.Rep.Eq as GEq
39+
import Data.Generic.Rep.Show as GShow
40+
import Data.Record.ShowRecord (showRecord)
41+
import Data.Maybe (Maybe(..), maybe)
42+
import Data.Either (Either(..))
43+
44+
45+
{- The types in question -}
46+
47+
newtype Percent = Percent Number
48+
data Point = Point Number Number
49+
50+
data Fill -- think of a paint program filling a shape
51+
= Solid Color
52+
| LinearGradient Color Color Percent
53+
| RadialGradient Color Color Point
54+
| NoFill
55+
56+
{------ Some samples to work with ------}
57+
58+
fillBlackToWhite :: Fill
59+
fillBlackToWhite = LinearGradient Color.black Color.white $ Percent 3.3
60+
61+
fillWhiteToBlack :: Fill
62+
fillWhiteToBlack = LinearGradient Color.white Color.black $ Percent 3.3
63+
64+
fillRadial :: Fill
65+
fillRadial = RadialGradient Color.white Color.black $ Point 1.0 3.4
66+
67+
68+
{------ Making prisms with Maybe and `prism'` ------}
69+
70+
-- `prism'` (note the apostrophe) takes two functions. One is a data
71+
-- constructor for the type in question. The other converts your
72+
-- desired case to a `Just <wrapped values>` or `Nothing`.
73+
74+
solidFocus :: Prism' Fill Color
75+
solidFocus = prism' constructor focus
76+
where
77+
constructor = Solid
78+
focus fill = case fill of
79+
Solid color -> Just color
80+
otherCases -> Nothing
81+
82+
-- In real life, you might abbreviate the above to this:
83+
84+
solidFocus' :: Prism' Fill Color
85+
solidFocus' = prism' Solid case _ of
86+
Solid color -> Just color
87+
_ -> Nothing
88+
89+
-- ... but being painfully explicit is better for examples.
90+
91+
92+
{------ Basic usage: `preview`, `review`, `is`, and `isn't` ------}
93+
94+
-- After building a prism, you focus in on a color with `preview`:
95+
96+
s1 :: Maybe Color
97+
s1 = preview solidFocus (Solid Color.white)
98+
-- (Just rgba 255 255 255 1.0)
99+
100+
s2 :: Maybe Color
101+
s2 = preview solidFocus fillRadial
102+
-- Nothing
103+
104+
-- ... or you can create a Fill from a color with `review`:
105+
106+
s3 :: Fill
107+
s3 = review solidFocus Color.white
108+
-- (Solid rgba 255 255 255 1.0)
109+
110+
-- ... or you can ask whether a given value matches the prism:
111+
112+
s4 :: Boolean
113+
s4 = is solidFocus (Solid Color.white) :: Boolean
114+
-- true
115+
116+
s5 :: Boolean
117+
s5 = isn't solidFocus (Solid Color.white) :: Boolean
118+
-- false
119+
120+
121+
{------ Making prisms with Either and `prism` ------}
122+
123+
-- Since `LinearGradient` wraps two colors and a percent, they need to
124+
-- be bundled together into a single value for `preview` to
125+
-- return. I'll use a record:
126+
127+
type LinearInterchange =
128+
{ color1 :: Color
129+
, color2 :: Color
130+
, percent :: Percent
131+
}
132+
133+
-- When making a prism with `prism` (no apostrophe), the "focus"
134+
-- function returns either the selected value (as a `Right`) or the
135+
-- entire argument (as a `Left`).
136+
137+
linearFocus :: Prism' Fill LinearInterchange
138+
linearFocus = prism constructor focus
139+
where
140+
constructor {color1, color2, percent} =
141+
LinearGradient color1 color2 percent
142+
focus = case _ of
143+
LinearGradient color1 color2 percent ->
144+
Right {color1, color2, percent}
145+
otherCases ->
146+
Left otherCases
147+
148+
-- Even though made differently than `solidFocus`, `linearFocus` is
149+
-- used the same way:
150+
151+
l1 :: String
152+
l1 = preview linearFocus fillBlackToWhite # maybe "!" showRecord
153+
-- "{ color1: rgba 0 0 0 1.0, color2: rgba 255 255 255 1.0, percent: (3.3%) }"
154+
155+
l2 :: Fill
156+
l2 = review linearFocus { color1 : Color.black
157+
, color2 : Color.white
158+
, percent : Percent 33.3
159+
}
160+
161+
162+
{------ Use `only` to focus on specific values ------}
163+
164+
whiteToBlackFocus :: Prism' Fill Unit
165+
whiteToBlackFocus = only fillWhiteToBlack
166+
167+
o1 :: Boolean
168+
o1 = is whiteToBlackFocus fillWhiteToBlack :: Boolean
169+
-- true
170+
171+
o2 :: Boolean
172+
o2 = is whiteToBlackFocus fillBlackToWhite :: Boolean
173+
-- false
174+
175+
o3 :: Boolean
176+
o3 = is whiteToBlackFocus fillRadial :: Boolean
177+
-- false
178+
179+
-- Note that `only` requires `Fill` to implement `Eq`.
180+
-- It's the only prism constructor that does.
181+
182+
{------ Use `nearly` to focus on a sub-case ------}
183+
184+
185+
-- `nearly` is typically used to look for a specific case (like other
186+
-- prisms), but also accepts only values that are close to some target
187+
-- value. It takes two values: a reference value, and a predicate that
188+
-- determines whether the wrapped value(s) are close enough to the
189+
-- reference. Note that the predicate takes the "whole" type (here,
190+
-- `Fill`), not the unwrapped values inside the case you care about.
191+
192+
-- In this example, we want to focus on solid colors that are "bright
193+
-- enough."
194+
195+
brightSolidFocus :: Prism' Fill Unit
196+
brightSolidFocus = nearly (Solid referenceColor) predicate
197+
where
198+
referenceColor = Color.graytone 0.8
199+
predicate = case _ of
200+
Solid color ->
201+
Color.brightness color >= Color.brightness referenceColor
202+
_ ->
203+
false
204+
205+
-- Because a `nearly` prism focuses into `Unit`, you can get only two
206+
-- values from `preview`:
207+
208+
n1 :: Maybe Unit
209+
n1 = preview brightSolidFocus (Solid Color.white)
210+
-- (Just unit)
211+
212+
n2 :: Maybe Unit
213+
n2 = preview brightSolidFocus (Solid Color.black)
214+
-- Nothing
215+
216+
n3 :: Maybe Unit
217+
n3 = preview brightSolidFocus NoFill
218+
-- Nothing
219+
220+
221+
-- ... so you probably want to use `is` or `isn't`:
222+
223+
n4 :: Boolean
224+
n4 = is brightSolidFocus (Solid Color.white) :: Boolean
225+
-- true
226+
227+
-- You can recover the reference value with `review`:
228+
229+
n5 :: Fill
230+
n5 = review brightSolidFocus unit
231+
-- (Solid rgba 204 204 204 1.0)
232+
233+
234+
235+
{------ Eq and Show are always nice ------}
236+
237+
-- ... although Eq is only required for `only`.
238+
239+
derive instance genericPercent :: Generic Percent _
240+
instance eqPercent :: Eq Percent where
241+
eq = GEq.genericEq
242+
instance showPercent :: Show Percent where
243+
show (Percent f) = "(" <> show f <> "%)"
244+
245+
derive instance genericPoint :: Generic Point _
246+
instance eqPoint :: Eq Point where
247+
eq = GEq.genericEq
248+
instance showPoint :: Show Point where
249+
show (Point x y) = "(" <> show x <> ", " <> show y <> ")"
250+
251+
derive instance genericFill :: Generic Fill _
252+
instance eqFill :: Eq Fill where
253+
eq = GEq.genericEq
254+
instance showFill :: Show Fill where
255+
show x = GShow.genericShow x

0 commit comments

Comments
 (0)