├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── elm.json └── src ├── Respond.elm └── Return.elm /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Isaac Shapira 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Return 2 | 3 | When writing Elm code you unavoidably have to interact with types like this: 4 | 5 | ```elm 6 | ( model, Cmd msg ) 7 | ``` 8 | 9 | This library makes that a Type: 10 | 11 | ```elm 12 | type alias Return msg model = 13 | ( model, Cmd msg ) 14 | ``` 15 | 16 | You can interact with `Return` in terms of the `Model`, and trust that `Cmd`s 17 | get tracked and batched together automatically. Letting you focus on composing 18 | transformations on `Return` without manually shuffling around `Cmd`. 19 | 20 | 21 | ## Examples 22 | 23 | Lets say we have a situation where we have a top level `update` function 24 | and wish to embed a component's `update` function. 25 | 26 | ```elm 27 | type Model 28 | = HomeMod Home.Model 29 | | AboutMod About.Model 30 | 31 | type Msg 32 | = HomeMsg Home.Msg 33 | | AboutMsg About.Msg 34 | 35 | update : Msg -> Model -> (Model, Cmd Msg) 36 | update msg model = 37 | let 38 | (updateModel, updateCmd) = 39 | case (model, msg) of 40 | (HomeMod model_, HomeMsg msg_) -> 41 | let 42 | (subModel, subCmd) = 43 | Home.update msg_ model_ 44 | 45 | in 46 | ( HomeMod subModel, Cmd.map HomeMsg subCmd ) 47 | 48 | (AboutMod model_, AboutMsg msg_) -> 49 | let 50 | (subModel, subCmd) = 51 | About.update msg_ model_ 52 | 53 | in 54 | ( AboutMod subModel, Cmd.map AboutMsg subCmd ) 55 | 56 | x -> 57 | let 58 | _ = 59 | Debug.log "Stray found" x 60 | 61 | in 62 | ( model, Cmd.none ) 63 | 64 | transformed = 65 | Route.alwaysTranfromModelWithMe updateModel 66 | 67 | in 68 | ( transformed 69 | , Cmd.batch 70 | [ updateCmd 71 | , alwaysDoMeCmd 72 | , conditionallyDoSomething transformed 73 | ] 74 | ) 75 | ``` 76 | 77 | The code above is good, but it suffers in some areas. For example 78 | we could forget to include `updateCmd` in the final `in` expression, 79 | and those side effects are lost. Or we could neglect to put the 80 | final *third* version of the model, `transformed`, into `conditionallyDoSomething`. 81 | That and there is a big dependence on pattern matching and literals, 82 | which does not lend itself well to pipelining. 83 | 84 | Lets see how we can clean this up with `Return`. 85 | 86 | ```elm 87 | type Model 88 | = HomeMod Home.Model 89 | | AboutMod About.Model 90 | 91 | type Msg 92 | = HomeMsg Home.Msg 93 | | AboutMsg About.Msg 94 | 95 | update : Msg -> Model -> Return Msg Model 96 | update msg model = 97 | (case (model, msg) of 98 | ( HomeMod model_, HomeMsg msg_ ) -> 99 | Home.update msg_ model_ 100 | |> mapBoth HomeMsg HomeMod 101 | 102 | ( AboutMod model_, AboutMsg msg_ ) -> 103 | About.update msg_ model_ 104 | |> mapBoth AboutMsg AboutMod 105 | 106 | x -> 107 | let 108 | _ = 109 | Debug.log "Stray found" x 110 | 111 | in 112 | singleton model) 113 | |> command alwaysDoMeCmd 114 | |> map Route.alwaysTranfromModelWithMe 115 | |> effect_ conditionallyDoSomething 116 | ``` 117 | 118 | The cleaned up code is not only more succinct, and more imperative (in a good way), 119 | but it doesn't suffer from the problems described above. There is no risk of mixing up 120 | your terms or accidentally dropping a cmd. 121 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "Fresheyeball/elm-return", 4 | "summary": "Return as a Writer Monad", 5 | "license": "BSD-3-Clause", 6 | "version": "7.1.0", 7 | "exposed-modules": [ 8 | "Return", 9 | "Respond" 10 | ], 11 | "elm-version": "0.19.0 <= v < 0.20.0", 12 | "dependencies": { 13 | "elm/core": "1.0.0 <= v < 2.0.0" 14 | }, 15 | "test-dependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /src/Respond.elm: -------------------------------------------------------------------------------- 1 | module Respond exposing (Respond, append, comap, sum, zero) 2 | 3 | {-| 4 | 5 | @docs Respond, append, sum, zero, comap 6 | 7 | -} 8 | 9 | 10 | {-| A function from a model to a Cmd. 11 | Basically there are times where you want to 12 | have a side effect on the world if the model 13 | has a certain shape. `Respond` facilitates 14 | this use case. 15 | -} 16 | type alias Respond msg a = 17 | a -> Cmd msg 18 | 19 | 20 | {-| -} 21 | append : Respond msg a -> Respond msg a -> Respond msg a 22 | append f g a = 23 | Cmd.batch [ f a, g a ] 24 | 25 | 26 | {-| -} 27 | sum : List (Respond msg a) -> Respond msg a 28 | sum rs a = 29 | List.map (\r -> r a) rs 30 | |> Cmd.batch 31 | 32 | 33 | {-| -} 34 | zero : Respond msg a 35 | zero = 36 | always Cmd.none 37 | 38 | 39 | {-| Add a function to the front 40 | `b -> a >> a -> Cmd msg` 41 | -} 42 | comap : (b -> a) -> Respond msg a -> Respond msg b 43 | comap = 44 | (>>) 45 | -------------------------------------------------------------------------------- /src/Return.elm: -------------------------------------------------------------------------------- 1 | module Return exposing 2 | ( Return, ReturnF 3 | , map, map2, map3, map4, map5, andMap, mapWith, mapCmd, mapBoth, dropCmd 4 | , piper, pipel, zero, piperK, pipelK 5 | , singleton, andThen, andThenK 6 | , return, command, effect_ 7 | , sequence, flatten, sequenceMaybe, traverseMaybe 8 | ) 9 | 10 | {-| 11 | 12 | 13 | ## Type 14 | 15 | Modeling the `update` tuple as a Monad similar to `Writer` 16 | 17 | @docs Return, ReturnF 18 | 19 | 20 | ## Mapping 21 | 22 | @docs map, map2, map3, map4, map5, andMap, mapWith, mapCmd, mapBoth, dropCmd 23 | 24 | 25 | ## Piping 26 | 27 | @docs piper, pipel, zero, piperK, pipelK 28 | 29 | 30 | ## Basics 31 | 32 | @docs singleton, andThen, andThenK 33 | 34 | 35 | ## Write `Cmd`s 36 | 37 | @docs return, command, effect_ 38 | 39 | 40 | ## Fancy non-sense 41 | 42 | @docs sequence, flatten, sequenceMaybe, traverseMaybe 43 | 44 | -} 45 | 46 | import Respond exposing (..) 47 | import Tuple 48 | 49 | 50 | {-| -} 51 | type alias Return msg model = 52 | ( model, Cmd msg ) 53 | 54 | 55 | {-| -} 56 | type alias ReturnF msg model = 57 | Return msg model -> Return msg model 58 | 59 | 60 | {-| -} 61 | piper : List (ReturnF msg model) -> ReturnF msg model 62 | piper = 63 | List.foldr (<<) zero 64 | 65 | 66 | {-| -} 67 | pipel : List (ReturnF msg model) -> ReturnF msg model 68 | pipel = 69 | List.foldl (>>) zero 70 | 71 | 72 | {-| -} 73 | zero : ReturnF msg model 74 | zero = 75 | identity 76 | 77 | 78 | {-| Transform the `Model`, the `Cmd` will be left untouched 79 | -} 80 | map : (a -> b) -> Return msg a -> Return msg b 81 | map f ( model, cmd ) = 82 | ( f model, cmd ) 83 | 84 | 85 | {-| Transform the `Model` of and add a new `Cmd` to the queue 86 | -} 87 | mapWith : (a -> b) -> Cmd msg -> Return msg a -> Return msg b 88 | mapWith f cmd ret = 89 | andMap ret ( f, cmd ) 90 | 91 | 92 | {-| Map an `Return` into a `Return` containing a `Model` function 93 | -} 94 | andMap : Return msg a -> Return msg (a -> b) -> Return msg b 95 | andMap ( model, cmd_ ) ( f, cmd ) = 96 | ( f model, Cmd.batch [ cmd, cmd_ ] ) 97 | 98 | 99 | {-| Map over both the model and the msg type of the `Return`. 100 | This is useful for easily embedding a `Return` in a Union Type. 101 | For example 102 | 103 | import Foo 104 | 105 | type Msg = Foo Foo.Msg 106 | type Model = FooModel Foo.Model 107 | 108 | ... 109 | 110 | update : Msg -> Model -> Return Msg Model 111 | update msg model = 112 | case msg of 113 | Foo foo -> Foo.update foo model.foo 114 | |> mapBoth Foo FooModel 115 | 116 | -} 117 | mapBoth : (a -> b) -> (c -> d) -> Return a c -> Return b d 118 | mapBoth f f_ ( model, cmd ) = 119 | ( f_ model, Cmd.map f cmd ) 120 | 121 | 122 | {-| Combine 2 `Return`s with a function 123 | 124 | map2 125 | (\modelA modelB -> { modelA | foo = modelB.foo }) 126 | retA 127 | retB 128 | 129 | -} 130 | map2 : 131 | (a -> b -> c) 132 | -> Return msg a 133 | -> Return msg b 134 | -> Return msg c 135 | map2 f ( x, cmda ) ( y, cmdb ) = 136 | ( f x y, Cmd.batch [ cmda, cmdb ] ) 137 | 138 | 139 | {-| -} 140 | map3 : 141 | (a -> b -> c -> d) 142 | -> Return msg a 143 | -> Return msg b 144 | -> Return msg c 145 | -> Return msg d 146 | map3 f ( x, cmda ) ( y, cmdb ) ( z, cmdc ) = 147 | ( f x y z, Cmd.batch [ cmda, cmdb, cmdc ] ) 148 | 149 | 150 | {-| -} 151 | map4 : 152 | (a -> b -> c -> d -> e) 153 | -> Return msg a 154 | -> Return msg b 155 | -> Return msg c 156 | -> Return msg d 157 | -> Return msg e 158 | map4 f ( w, cmda ) ( x, cmdb ) ( y, cmdc ) ( z, cmdd ) = 159 | ( f w x y z, Cmd.batch [ cmda, cmdb, cmdc, cmdd ] ) 160 | 161 | 162 | {-| -} 163 | map5 : 164 | (a -> b -> c -> d -> e -> f) 165 | -> Return msg a 166 | -> Return msg b 167 | -> Return msg c 168 | -> Return msg d 169 | -> Return msg e 170 | -> Return msg f 171 | map5 f ( v, cmda ) ( w, cmdb ) ( x, cmdc ) ( y, cmdd ) ( z, cmde ) = 172 | ( f v w x y z, Cmd.batch [ cmda, cmdb, cmdc, cmdd, cmde ] ) 173 | 174 | 175 | {-| Create a `Return` from a given `Model` 176 | -} 177 | singleton : model -> Return msg model 178 | singleton a = 179 | ( a, Cmd.none ) 180 | 181 | 182 | {-| 183 | 184 | foo : Model -> Return Msg Model 185 | foo ({ bar } as model) = 186 | -- forking logic 187 | if 188 | bar < 10 189 | -- that side effects may be added 190 | then 191 | ( model, getAjaxThing ) 192 | -- that the model may be updated 193 | 194 | else 195 | ( { model | bar = model.bar - 2 }, Cmd.none ) 196 | 197 | They are now chainable with `andThen`... 198 | 199 | resulting : Return msg { model | bar : Int } 200 | resulting = 201 | myReturn 202 | |> andThen foo 203 | |> andThen foo 204 | |> andThen foo 205 | 206 | Here we changed up `foo` three times, but we can use any function of 207 | type `(a -> Return msg b)`. 208 | 209 | Commands will be accumulated automatically as is the case with all 210 | functions in this library. 211 | 212 | -} 213 | andThen : (a -> Return msg b) -> Return msg a -> Return msg b 214 | andThen f ( model, cmd ) = 215 | let 216 | ( model_, cmd_ ) = 217 | f model 218 | in 219 | ( model_, Cmd.batch [ cmd, cmd_ ] ) 220 | 221 | 222 | {-| Construct a new `Return` from parts 223 | -} 224 | return : model -> Cmd msg -> Return msg model 225 | return a b = 226 | identity ( a, b ) 227 | 228 | 229 | {-| Add a `Cmd` to a `Return`, the `Model` is uneffected 230 | -} 231 | command : Cmd msg -> ReturnF msg model 232 | command cmd ( model, cmd_ ) = 233 | ( model, Cmd.batch [ cmd, cmd_ ] ) 234 | 235 | 236 | {-| Add a `Cmd` to a `Return` based on its `Model`, the `Model` will not be effected 237 | -} 238 | effect_ : Respond msg model -> ReturnF msg model 239 | effect_ f ( model, cmd ) = 240 | ( model, Cmd.batch [ cmd, f model ] ) 241 | 242 | 243 | {-| Map on the `Cmd`. 244 | -} 245 | mapCmd : (a -> b) -> Return a model -> Return b model 246 | mapCmd f ( model, cmd ) = 247 | ( model, Cmd.map f cmd ) 248 | 249 | 250 | {-| Drop the current `Cmd` and replace with an empty thunk 251 | -} 252 | dropCmd : ReturnF msg model 253 | dropCmd = 254 | singleton << Tuple.first 255 | 256 | 257 | {-| -} 258 | sequence : List (Return msg model) -> Return msg (List model) 259 | sequence = 260 | let 261 | f ( model, cmd ) ( models, cmds ) = 262 | ( model :: models, Cmd.batch [ cmd, cmds ] ) 263 | in 264 | List.foldr f ( [], Cmd.none ) 265 | 266 | 267 | {-| -} 268 | sequenceMaybe : Maybe (Return msg model) -> Return msg (Maybe model) 269 | sequenceMaybe = 270 | Maybe.map (map Just) >> Maybe.withDefault (singleton Nothing) 271 | 272 | 273 | traverseMaybe : (a -> Return msg model) -> Maybe a -> Return msg (Maybe model) 274 | traverseMaybe f = 275 | Maybe.map f >> sequenceMaybe 276 | 277 | 278 | {-| -} 279 | flatten : Return msg (Return msg model) -> Return msg model 280 | flatten = 281 | andThen identity 282 | 283 | 284 | {-| Kleisli composition 285 | -} 286 | andThenK : (a -> Return x b) -> (b -> Return x c) -> (a -> Return x c) 287 | andThenK x y a = 288 | x a |> andThen y 289 | 290 | 291 | {-| Compose updaters from the left 292 | -} 293 | pipelK : List (a -> Return x a) -> (a -> Return x a) 294 | pipelK = 295 | List.foldl andThenK singleton 296 | 297 | 298 | {-| Compose updaters from the right 299 | -} 300 | piperK : List (a -> Return x a) -> (a -> Return x a) 301 | piperK = 302 | List.foldr andThenK singleton 303 | --------------------------------------------------------------------------------