I'm trying to build a UI that allows a user to manipulate a recursive data structure. For example, imagine a visual schema editor or database table editor in which you have plain old types (strings and integers) and compound types made up of those plain types (arrays, structs). In the example below, a Struct_ is like a JavaScript object, where the keys are strings and the values are any type, including nested Array_s and Struct_s.
-- underscores appended to prevent confusion about native Elm types. These are custom to my application.
type ValueType
= String_
| Int_
| Float_
| Array_ ValueType
| Struct_ (List (String, ValueType))
type alias Field =
{ id : Int
, label : String
, hint : String
, hidden : Bool
, valueType : ValueType
}
type alias Schema = List Field
Now to go about building a UI for this I can make a simple recursive function:
viewField : Field -> Html Msg
viewField field =
div []
[ input [ type_ "text", value field.label ] []
, viewValueType field.valueType
]
viewValueType : ValueType -> Html Msg
viewValueType valueType =
let
structField : (String, ValueType) -> Html Msg
structField (key, subtype) =
div []
[ input [type_ "text", placeholder "Key", value key, onInput EditStructSubfieldKey] []
, viewValueType subtype
]
options : List(Html Msg)
options = case valueType of
String_ -> -- string ui
Int_ -> -- int ui
Float_ -> -- float ui
Array_ subtype ->
[ label [] [ text "subtype" ]
, viewValueType subtype
]
Struct_ fields ->
[ label [] [ text "subfields" ]
, List.map structField fields
, button [ onClick AddStructSubfield ] [ text "Add subfield" ]
]
in
div [] options
My issue arises when trying to manipulate my state with this recursive structure. What data structure in a Msgs would accommodate user edits to this structure, adding new fields, subfields, and editing their properties? How would I properly decode that in my update loop?
For example...
type alias Model =
{ fields : List Field }
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
AddStructSubfield _???_ ->
({model | fields = ???}, Cmd.none)
EditStructSubfieldKey _???_ ->
({model | fields = ???}, Cmd.none)
What kind of data would you attach to that AddStructSubfield or EditStructSubfieldKey message (that's passed with the onClick handler to the button above) to properly update your state, specifically when the Struct_ is say, nested inside of another Struct_, nested inside of an Array_? EditStructSubfieldKey, for example, will only contain the new string that the user has entered, but not enough information to address a deeply-nested item.
We do exactly this in our code base, but haven't open sourced the 'library' that supported this. But the answer to your question is that you need to add the notion of a Path to your code and messages.
type Path
= Field: String
| Index: Int
Then your view has to keep updating the path as you descend [Field "f1", Index 3, ...], and your update function needs to be supported by insert, delete,... that take a Path and the existing structure and return you a new one.
I ended up solving this by passing an updater function down the recursive chain. I've simplified this example as much as possible while showing the recursive nature of the updating. This allows for updating infinitely nested structures and lists without worrying about encoding/decoding a path. The downside, I believe, is that my single update Msg will always replace the entire model. I'm not sure about the semantics of how this will affect Elm's equality checking, and if that will produce performance issues in certain applications.
This example can be copy/pasted into https://elm-lang.org/try as-is to see it in action.
import Html exposing (Html, div, input, ul, li, text, select, button, option)
import Html.Attributes exposing (value, type_, selected)
import Html.Events exposing (onInput, onClick)
import Browser
type ValueType
= String_
| Int_
| Array_ ValueType
| Struct_ (List Field)
type alias Field =
{ label : String
, valueType : ValueType
}
type alias Model = Field
main = Browser.sandbox { init = init, update = update, view = view }
init : Model
init =
{ label = "Root Field", valueType = String_ }
type Msg
= UpdateField Field
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateField field ->
field
view : Model -> Html Msg
view model =
let
updater : Field -> Msg
updater field =
UpdateField field
in
div [] [ viewField updater model ]
viewField : (Field -> Msg) -> Field -> Html Msg
viewField updater field =
let
updateLabel : String -> Msg
updateLabel newLabel =
updater {field | label = newLabel}
updateValueType : ValueType -> Msg
updateValueType newValueType =
updater {field | valueType = newValueType}
in
li []
[ input [ type_ "text", value field.label, onInput updateLabel ] [ ]
, viewTypeOptions updateValueType field.valueType
]
viewTypeOptions : (ValueType -> Msg) -> ValueType -> Html Msg
viewTypeOptions updater valueType =
let
typeOptions = case valueType of
String_ ->
div [] []
Int_ ->
div [] []
Array_ subtype ->
let
subUpdater : ValueType -> Msg
subUpdater newType =
updater <| Array_ newType
in
div [] [ div [] [ text "Subtype" ], viewTypeOptions subUpdater subtype ]
Struct_ fields ->
let
fieldAdder : Msg
fieldAdder =
updater <| Struct_ ({label = "", valueType = String_} :: fields)
fieldUpdater : Int -> Field -> Msg
fieldUpdater index newField =
updater <| Struct_ <| replaceInList index newField fields
in
div []
[ ul [] (List.indexedMap (\i -> (viewField <| fieldUpdater i)) fields)
, button [ onClick fieldAdder ] [ text "+ Add Field" ]
]
isArray t = case t of
Array_ _ -> True
_ -> False
isStruct t = case t of
Struct_ _ -> True
_ -> False
stringToType str = case str of
"string" -> String_
"int" -> Int_
"array" -> Array_ String_
"struct" -> Struct_ []
_ -> String_
changeType str =
updater <| stringToType str
in
div []
[ select [ onInput changeType ]
[ option [ value "string", selected <| valueType == String_ ] [ text "String" ]
, option [ value "int", selected <| valueType == Int_ ] [ text "Integer" ]
, option [ value "array", selected <| isArray valueType ] [ text "Array" ]
, option [ value "struct", selected <| isStruct valueType ] [ text "Struct" ]
]
, typeOptions
]
replaceInList : Int -> a -> List a -> List a
replaceInList index item list =
let
head = List.take index list
tail = List.drop (index+1) list
in
head ++ [ item ] ++ tail
My main program has an update function of
update : Msg -> Model -> ( Model, Cmd Msg )
To communicate with sub-components we can add another variant and wrap our messages in a new message
type alias Model =
{ ...
, child : Child.Model
}
type Msg
= ...
| ChildMsg Child.Msg
update msg model =
case msg of
...
ChildMsg childMsg ->
let
( childModel, cmd ) =
Child.update childMsg model.child
updatedModel =
{ model | child = childModel }
childCmd =
Cmd.map ChildMsg cmd
in
( updatedModel, childCmd )
However this seem challenging if the type of my sub-component's update function does not match the parent. Consider a child with a polymorphic update function:
-- PolymorphicChild.elm
update : Msg a -> Model -> ( Model, Cmd (Msg a) )
When running a command from this module, I must wrap it
PolymorphicChild.someCommand : Cmd (Msg Foo)
PolymorphicChild.someCommand
|> Cmd.map PolymorphicChild
However, this produces a Msg (PolymorphicChild.Msg Foo), not the Msg PolymorphicChild.Msg my App is expecting.
The right side of (|>) is causing a type mismatch.
(|>) is expecting the right side to be a:
Cmd (PolyMorphicChild.Msg Foo) -> a
But the right side is:
Cmd Polymorphic.Msg -> Cmd Msg
I tried adding a polymorphic parameter to App.Msg
-- App.elm
type Msg a =
= ..
| PolymorphicChildMsg (PolymorphicChild.Msg a)
But it basically blows up my entire program. Every function involving App.Msg needs to somehow be changed to work with the new child component.
How can I unify the two types and get the two components working together?
I think the problem is that you're leaking too much information in your publicly exposed Msg type. Your use of the type parameter of Msg a seems limited to a known set of types, either an Author, Category, Post, or Tag. From skimming your code, it looks like it will never be anything but one of those four, so the fact that you are abstracting things in this manner should be kept inside of this module rather than exposing it and burdening any other code that may be pulling this in.
I think you need to move the abstraction down a level to avoid parameterizing your public Msg type. I would suggest having four concrete constructors for Msg instead of parameterizing it, and shift the abstraction down to a helper LoadInfo a type:
type alias LoadInfo a =
{ worker : Worker a
, url : Url
, result : Result Http.Error ( Int, List a )
}
type Msg
= LoadPost (LoadInfo Post)
| LoadCategory (LoadInfo Category)
| LoadTag (LoadInfo Tag)
| LoadAuthor (LoadInfo Author)
I am a relative beginner in Elm. I learned how to recur over records, and to create extensible records. My target application has tree-shaped data, that is to be rendered in several tree views.
In order for Elm to allow recursive models, one needs
a Union type for the children field,
a Maybe in that type definition, and
the lazy anonymous function construct if Json decoding is needed.
So far so good, but I cannot combine recursion with extension. Here below is a minimized version of how far I got. This program is capable of rendering either one of the two example types (Folder and Note), but not both. (see lines commented with -- NOTE and -- FOLDER).
The problem is with the kids function. Elm doesn't allow it to produce two different output types. I am stuck with either duplicating the code, or do without record extensions. Both seems like show-stoppers.
Is there a way to get this working with both, extension and recursion, and without code duplication?
Run on Ellie
module Main exposing (main)
import Html exposing (..)
import Maybe
-- MAIN
main = Html.beginnerProgram
{ model = init
, update = update
, view = view
}
-- MODEL
type alias Model =
{ folder : Folder
, note : Note
}
type alias Node a =
{ a | name : String
, children : Children a
}
type alias Folder =
{ name : String
, children : ChildFolders
}
type alias Note =
{ name : String
, children : ChildNotes
}
type Children a = Children a (Maybe (List (Node a)))
type ChildFolders = ChildFolders (Maybe (List Folder))
type ChildNotes = ChildNotes (Maybe (List Note))
-- INIT
init : Model
init = Model
(Folder "Parent F" someFolders)
(Note "Parent N" (ChildNotes Nothing))
someFolders : ChildFolders
someFolders = ChildFolders
( Just
( [ Folder "Child F1" (ChildFolders Nothing)
, Folder "Child F2" (ChildFolders Nothing)
, Folder "Child F3" (ChildFolders Nothing)
]
)
)
-- UPDATE
type Msg = NoOp
update : Msg -> Model -> Model
update msg model =
case msg of
NoOp -> model
-- VIEW
view : Model -> Html msg
view model =
div []
[ viewBranch model.folder -- FOLDER
-- , viewBranch model.note -- NOTE
]
-- viewBranch : (?) -> Html msg
viewBranch node =
uli
( text node.name
:: ( node
|> kids
|> List.map viewBranch
)
)
uli : List (Html msg) -> Html msg
uli items = ul [] [ li [] items ]
-- kids : (?) -> (?)
kids { children } =
case children of
(ChildFolders data) -> Maybe.withDefault [] data -- FOLDER
-- (ChildNotes data) -> Maybe.withDefault [] data -- NOTE
In the Elm checkboxes example an Action is passed to the tag argument of the checkbox function (lines 51-53).
I don't understand how the type signature for this argument is (Bool -> Action) and how on line 69 it's able to use the function composition operator << to transform the Bool from targetChecked into the complete Action type.
EDIT:
This question can be reduced down to "why does the following work?"
type Action = Edit Int
do : (Int -> Action) -> Action
do tag = tag(123)
result : Action
result = do(Edit)
When you define a union type, each tag of the union type becomes a defined value. So when you define:
type Action = Tick | NoOp
this also defines:
Tick : Action
NoOp : Action
When the union tag has arguments, it becomes a "constructor", a function:
type Action = Edit Int
Edit : Int -> Action
(These tags are also used as patterns that you can match on with the case-of construct. See also the documentation on the website. )
I am working on recursive modules in OCaml and I have some trouble accessing type fields.
If I try to do :
module A = struct type t = { name : string; } end
module A2 =
struct
include A
let getName (x:t) = x.name
end;;
Everything is alright. However, I need a more complex type, forcing me to define my type in a recursive module.
module rec B:Set.OrderedType =
struct
type t = {name: string; set : S.t}
let compare _ _ = 0
end
and S:Set.S = Set.Make (B);;
Everything still works perfectly. However, the following module is incorrect :
module B2 =
struct
include B
let get_name (x:t) = x.name
end;;
The returned error is "Unbound record field name". What is the problem ?
module rec B:Set.OrderedType =
Your recursive definition says that module B has the signature Set.OrderedType, which hides the definition of t and in this case, its projections. In Set.OrderedType, the type t is abstract, like this: type t;;.
If you want to show the definition of type t, it must be part of the signature. The first example works because you did not offer a signature for module A, so it was typed by default with a signature that exports everything.
The example below works for me with OCaml 4.02.1.
module rec B:
sig type t = { name:string ; set : S.t } val compare: t -> t -> int end
=
struct
type t = {name: string; set : S.t}
let compare _ _ = 0
end
and S:Set.S = Set.Make (B);;
The toplevel confirms the definition thus:
module rec B :
sig type t = { name : string; set : S.t; } val compare : t -> t -> int end
and S : Set.S