Created
November 15, 2021 15:44
-
-
Save JSuder-xx/c140073c14319e52453cfa3f87051817 to your computer and use it in GitHub Desktop.
Demonstration of using functions that return commands as a possible replacement for Message(s) and large reducer blocks.
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
-- Demonstrates | |
-- * Dispatching a function Model -> (Model, Msg) rather than a message subsequently pattern matching in the reducer. | |
-- * Benefits include | |
-- * Module Oriented Programming. Grouping related operations with the type. Improved transparency. Better organization. | |
-- * Good ergonomics for composing larger states from smaller througn the use of Lenses. | |
-- * Slightly more succinct. | |
-- * Drawbacks | |
-- * The need to aggregate Cmd's (effects) adds a bit of overhead, but then when have effects ever been elegant? | |
-- * Replay is lost because messages cannot be serialized. | |
-- * Logic cannot run in a web worker because functions will not serialize across boundary. | |
-- | |
-- NOTE: The logic for the application starts on line 54. Everything before that point consists of | |
-- definitions that would be found in a library but which are implemented here for a self-contained example. | |
-- | |
-- Paste this code into: http://elm-lang.org/try | |
module Main exposing (..) | |
import Browser | |
import Html exposing (Html, a, button, div, text) | |
import Html.Attributes exposing (style) | |
import Html.Events exposing (onClick) | |
import Random | |
{-| Updater (would belong to a library) | |
-} | |
type EffectUpdater a | |
= EffectUpdater (a -> ( a, Cmd (EffectUpdater a) )) | |
type alias Updater a = | |
a -> a | |
liftEffectUpdater : (a -> a) -> EffectUpdater a | |
liftEffectUpdater fn = | |
EffectUpdater (\a -> ( fn a, Cmd.none )) | |
updateWithEffect : EffectUpdater a -> a -> ( a, Cmd (EffectUpdater a) ) | |
updateWithEffect (EffectUpdater fn) a = | |
fn a | |
{-| Simple Lens (would belong to a library) | |
-} | |
type alias Lens a b = | |
{ get : a -> b | |
, set : a -> b -> a | |
} | |
lens : (a -> b) -> (( a, b ) -> a) -> Lens a b | |
lens get tupleSet = | |
{ get = get | |
, set = \a -> \b -> tupleSet ( a, b ) | |
} | |
over : Lens a b -> Updater b -> Updater a | |
over abL bUpdater a = | |
abL.set a (bUpdater <| abL.get a) | |
overEffect : Lens a b -> EffectUpdater b -> EffectUpdater a | |
overEffect abL (EffectUpdater bUpdater) = | |
EffectUpdater | |
(\a -> | |
let | |
( x, y ) = | |
bUpdater <| abL.get a | |
in | |
( abL.set a x, Cmd.map (overEffect abL) y ) | |
) | |
{-| Html Utility (would belong to a library) | |
-} | |
viewThroughLens : (b -> Html (EffectUpdater b)) -> a -> Lens a b -> Html (EffectUpdater a) | |
viewThroughLens bView a abLens = | |
bView (abLens.get a) |> Html.map (overEffect abLens) | |
main : Program () App (EffectUpdater App) | |
main = | |
Browser.element | |
{ init = | |
\() -> | |
( { left = Adder 0, right = Adder 10 }, Cmd.none ) | |
, update = updateWithEffect | |
, view = view | |
, subscriptions = \_ -> Sub.none | |
} | |
{-| Adder Module | |
-} | |
type Adder | |
= Adder Int | |
inc : Adder -> Adder | |
inc (Adder n) = | |
Adder (n + 1) | |
incRandom : EffectUpdater Adder | |
incRandom = | |
EffectUpdater | |
(\m -> | |
( m, Random.generate (\v -> liftEffectUpdater (\(Adder n) -> Adder (n + v))) (Random.int 1 10) ) | |
) | |
show : Adder -> String | |
show (Adder n) = | |
String.fromInt n | |
reset : Adder -> Adder | |
reset _ = | |
Adder 0 | |
viewAdder : Adder -> Html (EffectUpdater Adder) | |
viewAdder model = | |
div [] | |
[ div [ style "display" "inline-block", style "width" "40px" ] [ text (show model) ] | |
, button [ onClick (liftEffectUpdater inc) ] [ text "+" ] | |
, button [ onClick incRandom ] [ text "+Random" ] | |
, button [ onClick (liftEffectUpdater reset) ] [ text "Reset" ] | |
] | |
{-| App | |
-} | |
type alias App = | |
{ left : Adder | |
, right : Adder | |
} | |
leftL : Lens App Adder | |
leftL = | |
lens .left (\( a, b ) -> { a | left = b }) | |
rightL : Lens App Adder | |
rightL = | |
lens .right (\( a, b ) -> { a | right = b }) | |
incBoth : App -> App | |
incBoth { left, right } = | |
{ left = inc left | |
, right = inc right | |
} | |
incBothRandom : EffectUpdater App | |
incBothRandom = | |
EffectUpdater | |
(\{ left, right } -> | |
let | |
( newLeft, cmdLeft ) = | |
updateWithEffect incRandom left | |
( newRight, cmdRight ) = | |
updateWithEffect incRandom right | |
in | |
( { left = newLeft, right = newRight }, Cmd.batch [ cmdLeft |> Cmd.map (overEffect leftL), cmdRight |> Cmd.map (overEffect rightL) ] ) | |
) | |
view : App -> Html (EffectUpdater App) | |
view app = | |
div [] | |
[ div [] <| List.map (viewThroughLens viewAdder app) [ leftL, rightL ] | |
, button [ onClick (liftEffectUpdater incBoth) ] [ text "+ Both" ] | |
, button [ onClick incBothRandom ] [ text "+ Both Random" ] | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment