-
-
Notifications
You must be signed in to change notification settings - Fork 39
/
Monad.hs
231 lines (188 loc) Β· 6.13 KB
/
Monad.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
-- | Contains general underlying monad for bidirectional conversion.
module Toml.Bi.Monad
( Codec (..)
, BiCodec
, dimap
, dioptional
, diwrap
, (<!>)
, (.=)
) where
import Control.Applicative (Alternative (..), optional)
import Control.Monad (MonadPlus (..))
import Data.Coerce (Coercible, coerce)
{- | Monad for bidirectional conversion. Contains pair of functions:
1. How to read value of type @a@ from immutable environment context @r@?
2. How to store value of type @a@ in stateful context @w@?
In practice instead of @r@ we will use some @Reader Toml@ and instead of @w@ we will
use @State Toml@. This approach with the bunch of utility functions allows to
have single description for from/to @TOML@ conversion.
In practice this type will always be used in the following way:
@
type 'BiCodec' r w a = 'Codec' r w a a
@
Type parameter @c@ if fictional. Here some trick is used. This trick is
implemented in the [codec](http://hackage.haskell.org/package/codec) package and
described in more details in related blog post:
<https://blog.poisson.chat/posts/2016-10-12-bidirectional-serialization.html>.
-}
data Codec r w c a = Codec
{ -- | Extract value of type @a@ from monadic context @r@.
codecRead :: r a
-- | Store value of type @c@ inside monadic context @w@ and returning
-- value of type @a@. Type of this function actually should be @a -> w ()@ but with
-- such type it's impossible to have 'Monad' and other instances.
, codecWrite :: c -> w a
}
-- | Specialized version of 'Codec' data type. This type alias is used in practice.
type BiCodec r w a = Codec r w a a
instance (Functor r, Functor w) => Functor (Codec r w c) where
fmap :: (a -> b) -> Codec r w c a -> Codec r w c b
fmap f codec = Codec
{ codecRead = f <$> codecRead codec
, codecWrite = fmap f . codecWrite codec
}
instance (Applicative r, Applicative w) => Applicative (Codec r w c) where
pure :: a -> Codec r w c a
pure a = Codec
{ codecRead = pure a
, codecWrite = \_ -> pure a
}
(<*>) :: Codec r w c (a -> b) -> Codec r w c a -> Codec r w c b
codecf <*> codeca = Codec
{ codecRead = codecRead codecf <*> codecRead codeca
, codecWrite = \c -> codecWrite codecf c <*> codecWrite codeca c
}
instance (Monad r, Monad w) => Monad (Codec r w c) where
(>>=) :: Codec r w c a -> (a -> Codec r w c b) -> Codec r w c b
codec >>= f = Codec
{ codecRead = codecRead codec >>= \a -> codecRead (f a)
, codecWrite = \c -> codecWrite codec c >>= \a -> codecWrite (f a) c
}
instance (Alternative r, Alternative w) => Alternative (Codec r w c) where
empty :: Codec r w c a
empty = Codec
{ codecRead = empty
, codecWrite = \_ -> empty
}
(<|>) :: Codec r w c a -> Codec r w c a -> Codec r w c a
codec1 <|> codec2 = Codec
{ codecRead = codecRead codec1 <|> codecRead codec2
, codecWrite = \c -> codecWrite codec1 c <|> codecWrite codec2 c
}
instance (MonadPlus r, MonadPlus w) => MonadPlus (Codec r w c) where
mzero = empty
mplus = (<|>)
-- | Alternative instance for function arrow but without 'empty'.
infixl 3 <!>
(<!>) :: Alternative f => (a -> f x) -> (a -> f x) -> (a -> f x)
f <!> g = \a -> f a <|> g a
{- | This is an instance of @Profunctor@ for 'Codec'. But since there's no
@Profunctor@ type class in @base@ or package with no dependencies (and we don't
want to bring extra dependencies) this instance is implemented as a single
top-level function.
Useful when you want to parse @newtype@s. For example, if you had data type like
this:
@
__data__ Example = Example
{ foo :: Bool
, bar :: Text
}
@
Bidirectional TOML converter for this type will look like this:
@
exampleCodec :: TomlCodec Example
exampleCodec = Example
\<$\> Toml.bool "foo" '.=' foo
\<*\> Toml.text "bar" '.=' bar
@
Now if you change your type in the following way:
@
__newtype__ Email = Email { unEmail :: Text }
__data__ Example = Example
{ foo :: Bool
, bar :: Email
}
@
you need to patch your TOML codec like this:
@
exampleCodec :: TomlCodec Example
exampleCodec = Example
\<$\> Toml.bool "foo" '.=' foo
\<*\> 'dimap' unEmail Email (Toml.text "bar") '.=' bar
@
-}
dimap
:: (Functor r, Functor w)
=> (c -> d) -- ^ Mapper for consumer
-> (a -> b) -- ^ Mapper for producer
-> Codec r w d a -- ^ Source 'Codec' object
-> Codec r w c b -- ^ Target 'Codec' object
dimap f g codec = Codec
{ codecRead = g <$> codecRead codec
, codecWrite = fmap g . codecWrite codec . f
}
{- | Bidirectional converter for @Maybe a@ values. For example, given the data
type:
@
__data__ Example = Example
{ foo :: Bool
, bar :: Maybe Int
}
@
the TOML codec will look like
@
exampleCodec :: TomlCodec Example
exampleCodec = Example
\<$\> Toml.bool "foo" '.=' foo
\<*\> 'dioptional' (Toml.int "bar") '.=' bar
@
-}
dioptional
:: (Alternative r, Applicative w)
=> Codec r w c a
-> Codec r w (Maybe c) (Maybe a)
dioptional Codec{..} = Codec
{ codecRead = optional codecRead
, codecWrite = traverse codecWrite
}
{- | Combinator used for @newtype@ wrappers. For example, given the data types:
@
__newtype__ N = N Int
__data__ Example = Example
{ foo :: Bool
, bar :: N
}
@
the TOML codec can look like
@
exampleCodec :: TomlCodec Example
exampleCodec = Example
\<$\> Toml.bool "foo" '.=' foo
\<*\> 'diwrap' (Toml.int "bar") '.=' bar
@
-}
diwrap
:: forall b a r w .
(Coercible a b, Functor r, Functor w)
=> BiCodec r w a
-> BiCodec r w b
diwrap = dimap coerce coerce
{- | Operator to connect two operations:
1. How to get field from object?
2. How to write this field to toml?
In code this should be used like this:
@
__data__ Foo = Foo
{ fooBar :: Int
, fooBaz :: String
}
fooCodec :: TomlCodec Foo
fooCodec = Foo
\<$\> Toml.int "bar" '.=' fooBar
\<*\> Toml.str "baz" '.=' fooBaz
@
-}
infixl 5 .=
(.=) :: Codec r w field a -> (object -> field) -> Codec r w object a
codec .= getter = codec { codecWrite = codecWrite codec . getter }