-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #88 from marick/master
Step 1 toward Prism documentation: a "how to" module for Prisms.
- Loading branch information
Showing
5 changed files
with
338 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |