How do I merge signals of nested tagged types? - elm

As part of the Elm app I’m building, I want to keep signals of environment changes (like resizing the window) from data changes (rendering a filterable list of models to the browser). I thought I would model these as different extensible types:
type WindowUpdate = Resize (Int, Int)
type DataUpdate = TagFilter Model.Tag
type Update update data = WindowUpdate update data
| DataUpdate update data
| NoOp
updates : Signal.Mailbox (Update update data)
updates = Signal.mailbox NoOp
appModel : Signal Model
appModel =
let
applicationUpdates = Signal.mergeMany
[ updates.signal
]
in
Signal.foldp update Model.defaultModel applicationUpdates
windowUpdate : WindowUpdate -> Model -> Model
windowUpdate update model =
let resizeWidth = \windowModel newWidth -> { windowModel | width = newWidth }
in
case update of
Resize (w, _) -> { model | window = (resizeWidth model.window w) }
update : Update -> Model -> Model
update u model =
case u of
WindowUpdate wu data -> windowUpdate (wu data) model
DataUpdate du data -> model
otherwise -> model
Unfortunately I can’t get my update function to work correctly. I get the following compiler errors:
— TYPE MISMATCH —————————————————————— ./app/Updates.elm
The 3rd argument to function `foldp` is causing a mismatch.
36│ Signal.foldp update Model.defaultModel applicationUpdates
^^^^^^^^^^^^^^^^^^
Function `foldp` is expecting the 3rd argument to be:
Signal (Update a)
But it is:
Signal Update
Hint: I always figure out the type of arguments from left to right. If an
argument is acceptable when I check it, I assume it is "correct" in subsequent checks. So the problem may actually be in how previous arguments interact with the 3rd.
What am I doing wrong?

You forgot the type parameters of Update in the signature of update, it should be (code untested):
update : Update update data -> Model -> Model

Related

Update record field from Collection

I am playing a little bit with Elm these days, but I stuck with a simple case where I want to update a record field. My code is like this:
-- MODEL
initialModel : Model
initialModel =
{ selectedLanguage = "german"
, allCards = Card.cards
}
type alias Msg =
{ description : String
, data : String
, id : String
}
The update function
update : Msg -> Model -> Model
update msg model =
case List.head (model.allCards) of
Just card ->
{ card | fliped = True }
Nothing -> model
but I see this:
Something is off with the 1st branch of this `case` expression:
50| { card | fliped = True }
^^^^^^^^^^^^^^^^^^^^^^^^
The 1st branch is a record of type:
{ back : String, fliped : Bool, front : String, id : String }
But the type annotation on `update` says it should be:
Model
Hint: Seems like a record field typo. Maybe back should be allCards?
Hint: Can more type annotations be added? Type annotations always help me give
more specific messages, and I think they could help a lot in this case!
Detected errors in 1 module.
I think I should always return a model from update function like my type says, but cannot figure out how. Any advice here?
You'll have update the allCards field of model too. You can nest the card update inside the model update if the former returns a list instead of just a single card:
update : Msg -> Model -> Model
update msg model =
{ model
| allCards =
case model.allCards of
card :: rest ->
{ card | fliped = True } :: rest
[] ->
[]
}
Or you can bind the new allCards to a name if you prefer:
update : Msg -> Model -> Model
update msg model =
let
newAllCards =
case model.allCards of
card :: rest ->
{ card | fliped = True } :: rest
[] ->
[]
in
{ model | allCards = newAllCards }
I pattern match directly on the list here instead of using List.head, as that also gives me the remainder of the list and I don't have to deal with an intermediary Maybe value (or two actually, since List.tail returns a Maybe as well). The card::rest branch hits if allCards contains at least one card, so the only remaining case is therefore [], which is easy enough to handle.
Also, flipped is spelled with two ps ;)

Command not being invoked after calling update

Building on the Elm navigation tutorial, I needed to execute a command to fetch additional data once navigating to my CategoryRoute.
My View.elm looks something like this:
view : Model -> Html Msg
view model =
div []
[ page model ]
page : Model -> Html Msg
page model =
case model.categories of
RemoteData.Success categories ->
case model.currentRoute of
CategoryListRoute ->
CategoryList.view categories
CategoryRoute id ->
let maybeCategory =
categories
|> SubCategories
|> flatten
|> filter (\category -> category.id == id)
|> head
_ = update (OnCategorySelected id) model
in
case maybeCategory of
Just category ->
Category.view category
Nothing ->
notFound
You'll notice that I'm calling update with the OnCategorySelected message myself when the currentRoute changes to the CategoryRoute.
My Update.eml looks something like this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnArticlesFetch response ->
let
_ = log "got response" response
in
( { model | articles = response }, Cmd.none)
OnLocationChange location ->
let
newRoute =
parseLocation location
in
( { model | currentRoute = newRoute }, Cmd.none )
OnCategorySelected id ->
( model, (getArticles model.tenant id) )
And finally, my Commands.eml looks like so:
getArticles : String -> String -> Cmd Msg
getArticles tenant id =
let
url =
"https://" ++ tenant ++ ".my.api"
_ = log "getArticles for " id
in
Http.post url (Http.jsonBody (encoder id)) decoder
|> RemoteData.sendRequest
|> Cmd.map OnArticlesFetch
I was expecting that once I'll call update OnCategorySelected, it will in turn invoke the getArticles function, which is passed a Cmd Msg, which I had thought will be invoked once the response comes in.
The problem I'm facing is that while update OnCategorySelected and getArticles seem to get invoked (as indicated by the log printouts log "getArticles for " id), I'm seeing no outgoing HTTP calls, no errors, no results and no log "got response" response printouts.
I'm confused as to what am I doing wrong here and what's the pattern for actually fetching more data as one navigates to a page in Elm...
Elm is a pure language where side effects are relegated to the framework. Calling the update function does not actually perform any work itself. It simply returns a value that can be handed off to the Elm framework directing it to interact with the outside world. That means when you call update from within the page function and discard the result, nothing happens.
One thing that can cause confusion is that Debug.log actually does get called and prints to the console, which violates the aforementioned purity of the language. It's just a magic function which exists only for debugging so hopefully it doesn't cause too much confusion.
You should instead be handling the RemoteData.Success case in the update function after parsing the route in the OnLocationChange case and returning a result which includes the getArticles result Cmd.
OnLocationChange location ->
let
newRoute =
parseLocation location
cmd =
case newRoute of
CategoryRoute id ->
getArticles model.tenant id
_ ->
Cmd.none
in
( { model | currentRoute = newRoute }, cmd )

Locally scoped updates in elm 0.18

I have an elm 0.18 web app with a number of pages and routes. In main.elm I define my update function.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FirstUpdateAction ->
...
Every action goes through this function and it's getting big. Is it possible to create an update function to a smaller module that is nested within the overall structure?
For example, I have a settings page that gives the user the ability to change password. There are three fields/states (passwordOld, passwordNew, passwordConfirm) which have update actions associated with onInput and onBlur events. Those states and actions are only relevent to the user settings page, and become irrelevent to the rest of the model when the user leaves the page.
How could I go about setting up a scope for the user settings?
You could break down your code into independent submodules, each with it's own Msg type, update and view functions.
For example you could have a file SubmoduleA.elm looking like this:
module SubmoduleA exposing (Model, Msg, update, view)
type Msg = SubMessageA
| SubMessageB
[..]
type alias model =
{ fieldA : TypeA
, fieldB : TypeB
, [..]
}
update msg model =
case msg of
MessageA ->
{model | fieldA = [..] } ! []
[..]
view model =
div [id "submoduleAView"]
[..]
this module would be connected to your main program like this:
module Main exposing (..)
import SubmoduleA exposing (Model, Msg, update, view)
type Msg = MessageA
| MessageB
| ToSubmoduleA (SubmoduleA.Msg)
[..]
type alias model =
{ fieldA : TypeA
, fieldB : TypeB
, [..]
, subModuleA : SubmoduleA.Model
}
update msg model =
case msg of
MessageA ->
{model | fieldA = [..] } ! []
[..]
ToSubmoduleA msg =
let (newSubmoduleA, newSubmoduleACmd) = SubmoduleA.update msg (.subModuleA model)
in { model | subModuleA = newSubmoduleA } ! [Cmd.map ToSubmoduleA newSubmoduleACmd]
view model =
div [id "mainView"]
[ ..
, Html.map ToSubmoduleA <| SubmoduleA.view (.subModuleA model)
]
this way all the information and state that are relevant to your sub module stay encapsulated in your sub module, and you just have one case in your main update function responsible for the correct routing of messages.

Type error in the update function in Elm

I'm new to elm (0.17) and I try to understand how it works. In this case, I try to develop a kind of project estimation.
This is what I did:
import Html exposing (..)
import Html.App as Html
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- model
type alias Host = {
name : String,
cost : Int
}
type alias Model =
{ email : String
, hosting : List Host
, period : List Int
, interventionDays : List Int
, total : Int
}
init : (Model, Cmd Msg)
init =
(Model "init#email.fr" [{name="AAA", cost=15}, {name="BBB", cost=56}, {name="CCC", cost=172}] [1..12] [1..31] 0, Cmd.none)
type Msg = Submit | Reset
calculate : Int
calculate = 42 -- to test
update : Msg -> Model -> (Model, Cmd Msg)
update action model =
case action of
Submit ->
(model, calculate)
Reset ->
(model, Cmd.none)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- view
hostOption host =
option [ value (toString host.cost) ] [ text host.name ]
durationOption duration =
option [value (toString duration) ] [ text (toString duration)]
view : Model -> Html Msg
view model =
Html.form []
[ h2 [] [ text "Estimate your project"]
, input [ placeholder model.email ] []
, select []
(List.map hostOption model.hosting)
, select []
(List.map durationOption model.period)
, select []
(List.map durationOption model.interventionDays)
, Html.span [][text (toString model.total)]
, button [onClick Submit] [text "Submit"]
, button [onClick Reset] [text "Reset"]
]
I think I have understood some ideas behind elm but I need help because elm-make command returns:
The 1st and 2nd branches of this `case` produce different types of values.
40| case action of
41| Submit ->
42| (model, calculate)
43| Reset ->
44|> (model, Cmd.none)
The 1st branch has this type:
( a, Int )
But the 2nd is:
( a, Cmd a )
Hint: All branches in a `case` must have the same type. So no matter which one
we take, we always get back the same type of value.
Detected errors in 1 module.
I understand the problem but I do not know how to fix it. Do I have to define my calculate function to work with model data ?
Thanks
I'm going to guess that you want to update the the total field of your model with calculate.
The first tuple item that the update function returns is the updated model. As things stand, both of your actions return the existing model without changing it. So you could try this:
case action of
Submit ->
({ model | total = calculate }, Cmd.none)
Reset ->
init
See here for the syntax for updating records.
Note that I also changed the Reset branch to return init, the initial model and command.
The compiler error is telling you that the update method, in some cases will return a (Model, Cmd) tuple, and in another cases will return a (Model, Int) tuple.
The update function as you have it, should return the modified model and also a Cmd to execute an action, in other words, a (Model, Cmd) tuple.
If you return (model, calculate) it will return a (Model, Int) tuple, since calculate is an Int. That is what is breaking the compiling.
So to fix it, first you need to decide what to do with each of the Msg. I assume by the name of them that the Calculate Msg will update the total and the Reset Msg will set the model to the default state.
For that you could do:
case action of
Submit ->
({ model | total = calculate }, Cmd.none)
Reset ->
init
In this case, both branches will return a tuple of type (Model, Cmd).
Note that the Reset branch will return init, which is already of type (Model, Cmd).
Check the official guide for more examples: http://guide.elm-lang.org/index.html

How to propagate clicks signal to model update in Elm?

I'm writing a game in Elm and in this game there's a button that when pressed should reset the game board to its initial state. I don't understand however how a button click signal is to be propagated to the board model's update function. In the code below, I've just passed the unit value to stepBoard but I can't do much with it so how can I solve it?
--MODEL
type Board = [(Int,Int)]
type Game = {name: String, board: Board}
defaultGame = {name = "Test", board = [(3,3)]}
--UPDATE
stepBoard click (colsInit, rowsInit) (rows, cols) g =
{g | board <- if ? then --some initial value (colsInit, rowsInit)
else --some updated value using (rows, cols)}
stepGame: Input -> Game -> Game
stepGame {click, name, rowsColsInit, rowsCols} g = stepBoard click (0,0) (0,0) g
game = foldp stepGame defaultGame input
--INPUT
type Input = {click:(), name: String, rowsColsInit: (Int,Int), rowsCols: (Int,Int)}
input = Input <~ clicks ~ nameSignal ~ rowsColsInitSignal ~ rowsColsSignal
--VIEW
(btn, clicks) = Input.button "Click"
Combining foldp and click information can be unintuitive.
The usual solution employed is an ADT to encode the different possible events:
data Input = Init (Int, Int)
| Update { name : String
, rowsCols : ( Int, Int )
}
input = merge (Init <~ (sampleOn clicks rowsColsInitSignal))
(OtherInput <~ nameSignal ~ rowsColsSignal)
merge merges two signals and always creates a constant function.
Now you can just match by case in your update function:
stepBoard input g =
let newBoard =
case input of
Init (initRows, initCols) -> -- do your initialisation here
Update {name, rowsCols} -> -- do your update here
in { g | board <- newBoard }