Skip to content

Commit d356701

Browse files
author
Tom Harding
committed
OneOf
Generalised Either. I think it might be called Variant?
0 parents  commit d356701

File tree

10 files changed

+526
-0
lines changed

10 files changed

+526
-0
lines changed

.gitignore

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
*.aux
2+
*.chi
3+
*.chs.h
4+
*.dyn_hi
5+
*.dyn_o
6+
*.eventlog
7+
*.hi
8+
*.hp
9+
*.o
10+
*.prof
11+
*~
12+
.HTF/
13+
.cabal-sandbox/
14+
.ghc.environment.*
15+
.hpc
16+
.hsenv
17+
.stack-work/
18+
cabal-dev
19+
cabal.project.local*
20+
cabal.sandbox.config
21+
dist
22+
dist-*
23+
experiments.cabal

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 Tom Harding
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Learning Me a Haskell 🎩✨
2+
3+
It's about time I figured out what all the fuss is about. I'll keep all my
4+
findings in this repo.
5+
6+
## [OneOf](/src/OneOf/Types.hs#L30-L32)
7+
8+
### Intro
9+
10+
`OneOf` is a generalised version of `Either` (I think sometimes known as a
11+
`Variant`?). While `Either` is strictly for one of two possibilities, `OneOf`
12+
generalises this to any (non-zero) number of possibilities with a type-level
13+
list. For example:
14+
15+
```haskell
16+
f :: OneOf '[String] -- `String`
17+
g :: OneOf '[String, Bool] -- `Either String Bool`
18+
h :: OneOf '[String, Bool, Int] -- `Either String (Either Bool Int)`
19+
```
20+
21+
Rather than using `Left` and `Right`, we construct these values with some
22+
number of `There`s followed by a `Here`. For example:
23+
24+
```haskell
25+
f :: OneOf '[String]
26+
f = Here "Hello!"
27+
28+
g :: OneOf '[String, Bool]
29+
g = There (Here True)
30+
31+
h :: OneOf '[String, Bool, Int]
32+
h = There (There (Here 3))
33+
```
34+
35+
### Injection
36+
37+
The above is quite... ugly, though, right? What we'd really like is a neat way
38+
to "lift" a type into the `OneOf` without having to worry about the number of
39+
`There`s we need. Luckily, the `inject` function gives us just that:
40+
41+
```haskell
42+
f :: OneOf '[String]
43+
f = inject "Hello"
44+
45+
g :: OneOf '[String, Bool]
46+
g = inject True
47+
48+
h :: OneOf '[String, Bool, Int]
49+
h = inject (3 :: Int) -- 3 is too polymorphic without annotation.
50+
```
51+
52+
`inject` looks through the list for the first occurrence of our type, and
53+
produces the constructors required to lift our value into the `OneOf`.
54+
55+
### Projection
56+
57+
Cool, we have a type that could hold a value that is one of any number of
58+
types... how do we _use_ it? Well, we could pattern-match, but then we're back
59+
to worrying about everything `Here` and `There`. To help with this, the `fold`
60+
function will generate a Church-style fold for any `OneOf` value:
61+
62+
```haskell
63+
f :: OneOf '[String]
64+
-> (String -> result)
65+
-> result
66+
f = fold
67+
68+
g :: OneOf '[String, Bool]
69+
-> (String -> result)
70+
-> (Bool -> result)
71+
-> result
72+
g = fold
73+
74+
h :: OneOf '[String, Bool, Int]
75+
-> (String -> result)
76+
-> (Bool -> result)
77+
-> (Int -> result)
78+
-> result
79+
h = fold
80+
```
81+
82+
The type is calculated based on the type of the `OneOf` supplied as a first
83+
argument, and the pattern is always the same: _give me a function from each
84+
type to some `r`, and I'll just call the appropriate one._
85+
86+
Alternatively, we can fold a `OneOf` using a constraint, assuming that all
87+
values have an instance:
88+
89+
```haskell
90+
h :: OneOf '[String, Bool, Int] -> String
91+
h = interpret @Show show
92+
```
93+
94+
Here, we use the fact that `String`, `Bool`, and `Int` all have `Show`
95+
instances, and thus have an instance for the `show` function that returns a
96+
`String`. Now, we'll just get the `show` result for whatever is in our value.

Setup.hs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Distribution.Simple
2+
3+
main
4+
= defaultMain

package.yaml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: experiments
2+
version: 0.1.0.0
3+
github: "githubuser/experiments"
4+
license: BSD3
5+
author: "Author name here"
6+
maintainer: "[email protected]"
7+
copyright: "2018 Author name here"
8+
9+
extra-source-files:
10+
- README.md
11+
12+
description: Please see the README on GitHub at <https://github.com/githubuser/experiments#readme>
13+
14+
dependencies:
15+
- aeson
16+
- base
17+
- generic-lens
18+
- text
19+
- validation
20+
21+
library:
22+
source-dirs: src
23+
24+
tests:
25+
experiments-test:
26+
main: Spec.hs
27+
source-dirs: test
28+
ghc-options:
29+
- -threaded
30+
- -rtsopts
31+
- -with-rtsopts=-N
32+
dependencies:
33+
- experiments

src/OneOf.hs

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{-|
2+
Module : OneOf
3+
Description : A generalised @Either@ type.
4+
Copyright : (c) Tom Harding, 2018
5+
License : MIT
6+
Maintainer : [email protected]
7+
Stability : experimental
8+
9+
The @Either@ type has two constructors – @Left@ and @Right@ – to represent one
10+
of two possible options. If we wanted to represent three possible options, we
11+
could nest an @Either@ inside another, giving us @Left x@, @Right (Left y)@,
12+
and @Right (Right z)@ to represent our three options. We can do this forever,
13+
as long as we're happy to deal with the types that it will inflict upon us.
14+
15+
The @OneOf@ type is a generalisation of this idea: instead of nesting the type,
16+
we declare the number of constructors we want using a type-level list of types.
17+
We can, for example, give an isomorphism between @Either a b@ and
18+
@OneOf '[a, b]@, and it is clear to see how we could extend this to a third,
19+
fourth, and so on!
20+
-}
21+
module OneOf
22+
( OneOf (..)
23+
24+
-- * Convenience functions
25+
--
26+
-- We can work with a @OneOf@ directly using its constructors, but this
27+
-- becomes a pain as we generalise to larger sets of types. Consequently,
28+
-- we have three functions to make things a little easier.
29+
30+
, interpret
31+
, inject
32+
, fold
33+
) where
34+
35+
import OneOf.Fold (fold, interpret)
36+
import OneOf.Inject (inject)
37+
import OneOf.Types (OneOf (..))

src/OneOf/Fold.hs

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
{-# OPTIONS_HADDOCK not-home #-}
2+
3+
{-# LANGUAGE AllowAmbiguousTypes #-}
4+
{-# LANGUAGE ConstraintKinds #-}
5+
{-# LANGUAGE FlexibleContexts #-}
6+
{-# LANGUAGE FlexibleInstances #-}
7+
{-# LANGUAGE MultiParamTypeClasses #-}
8+
{-# LANGUAGE RankNTypes #-}
9+
{-# LANGUAGE ScopedTypeVariables #-}
10+
{-# LANGUAGE TypeApplications #-}
11+
{-# LANGUAGE TypeFamilies #-}
12+
{-# LANGUAGE TypeInType #-}
13+
{-# LANGUAGE TypeOperators #-}
14+
{-# LANGUAGE UndecidableInstances #-}
15+
16+
{-|
17+
Module : OneOf.Fold
18+
Description : Functions for working with a 'OneOf' value.
19+
Copyright : (c) Tom Harding, 2018
20+
License : MIT
21+
Maintainer : [email protected]
22+
Stability : experimental
23+
24+
Given a @OneOf '[x, y, z]@, there are two sensible ways to interpret this:
25+
26+
- If everything in its list – @[x, y, z]@ – satisfy some constraint @c@, and we
27+
can write some @∀x. c x => x -> r@, then we can call this function on the
28+
value, regardless of its position, and produce some result.
29+
30+
- Instead of using implementations of a typeclass, we can produce a fold,
31+
taking functions to work with each of the possible branches.
32+
33+
There are trade-offs with both. The first method leads to much neater code, as
34+
we can simply write `interpret f xs` and get our "value" from inside. With the
35+
latter, however, we'll save on boilerplate in cases where we would potentially
36+
like many interpretations of the same value. The choice is yours.
37+
-}
38+
module OneOf.Fold where
39+
40+
import Data.Kind (Constraint, Type)
41+
import OneOf.Types (OneOf(..))
42+
43+
-- | Interpret a @OneOf xs@ value, using knowledge that every member of @xs@
44+
-- satisfies the given constraint, @c@.
45+
46+
class Interpret (c :: Type -> Constraint) (xs :: [Type]) where
47+
48+
-- | The usual rank-2 trick: "give me a function that relies on @c@'s
49+
-- interface to produce some value, and I'll produce that value".
50+
51+
interpret :: (forall x. c x => x -> r) -> OneOf xs -> r
52+
53+
instance c x => Interpret c '[x] where
54+
55+
-- | Interpret a singleton list. Nothing especially clever: we know by
56+
-- construction that we must be looking at the type that is actually inside
57+
-- the 'OneOf', so we can just call the function on it.
58+
59+
interpret f (Here x) = f x
60+
interpret _ (There _) = error "Impossible"
61+
62+
instance (tail ~ (x' ': xs), Interpret c tail, c x)
63+
=> Interpret c (x ': x' ': xs) where
64+
65+
-- | For a non-singleton list (empty lists are impossible within 'OneOf'), we
66+
-- pattern match on whether we've found it or not. If we have, we do as we
67+
-- did above with the singleton. If we don't, we recurse deeper into the
68+
-- 'OneOf'.
69+
--
70+
-- Note that we can avoid an overlapping instance by pattern-matching @tail@
71+
-- a @Cons@ deeper.
72+
73+
interpret f (Here x ) = f x
74+
interpret f (There x) = interpret @c f x
75+
76+
-- | A @OneOf '[x, y]@ can be folded with @(x -> r) -> (y -> r) -> r@ – we just
77+
-- take a function for each possible value, and then apply the one that is
78+
-- relevant. This type family produces the signature, given the list.
79+
80+
type family FoldSignature (xs :: [Type]) r where
81+
-- More recursion: take the head, add its fold, recurse.
82+
FoldSignature (x ': xs) r = (x -> r) -> FoldSignature xs r
83+
84+
-- If we have no inputs, here's our output!
85+
FoldSignature '[] r = r
86+
87+
-- | This type class builds the fold for a given 'OneOf' value. Once that has
88+
-- happened, usage is straightforward:
89+
--
90+
-- > fold (inject True :: OneOf '[Int, String, Bool])
91+
-- > (\_ -> "Int!")
92+
-- > (\case
93+
-- > "hello" -> "String!"
94+
-- > _ -> "Rude string :(")
95+
-- > (\x -> if x then "YAY" else "BOO")
96+
--
97+
-- We can now fold out of our datatype just as we would with @either@.
98+
99+
class BuildFold xs result where
100+
101+
-- | Fold a 'OneOf' value by providing a function for each possible type.
102+
--
103+
-- >>> :t fold (undefined :: OneOf '[a, b])
104+
-- fold (undefined :: OneOf '[a, b])
105+
-- :: (a -> result) -> (b -> result) -> result
106+
107+
fold :: OneOf xs -> FoldSignature xs result
108+
109+
instance BuildFold '[x] result where
110+
111+
-- | For a singleton list (again, empties are impossible), we needn't do
112+
-- anything too clever: we just apply the function to the head.
113+
114+
fold (Here x) f = f x
115+
fold (There _) _ = error "Impossible"
116+
117+
instance (tail ~ (x' ': xs), BuildFold tail result, Ignore tail result)
118+
=> BuildFold (x ': x' ': xs) result where
119+
120+
-- | Things get more tricky for a non-singleton list if you find a match; how
121+
-- do you pass it over all the arguments in your fold?
122+
--
123+
-- Again, we avoid an overlapping instance by matching @tail@ as a @Cons@.
124+
125+
fold (There x) _ = fold @_ @result x
126+
fold (Here x) f = ignore @tail (f x)
127+
128+
class Ignore (args :: [Type]) result where
129+
130+
-- | @Ignore@ is a class whose only purpose is to generate n-ary @const@
131+
-- functions. Give it a result, and it'll figure out the type of a function
132+
-- that ignores all its arguments.
133+
134+
ignore :: result -> FoldSignature args result
135+
136+
-- | An empty list of arguments means we're ready to return the result!
137+
instance Ignore '[] result where
138+
ignore result = result
139+
140+
instance Ignore xs result
141+
=> Ignore (x ': xs) result where
142+
143+
-- | Provided I can ignore the tail, I can remove the whole thing. How? I
144+
-- just insert another argument and ignore that as well!
145+
146+
ignore result _ = ignore @xs result

0 commit comments

Comments
 (0)