Hierarchical data modeling

I am new to Elm. I try to find out if it might be useful for me. But I am a bit disturbed by the following threads:

I would like to write an application with several forms. Each form should contain several different inputs of different types and some inputs have themself a sub structure as for example dates (year, month, day). In every other language (JavaScript, Go, Scheme) I would create some nested records to build a tree for my data.

  • form a
    • entry a
      • input a
      • input b
    • entry b
      • input a
      • input b
  • form b
    • entry a
      • input a

and so on

But in the cited threads I found the following statements:

Elm is very opinionated and part of that opinion is that nesting is often a hint at bad design.

Although the language permits doing this, it is so strongly discouraged as a technique that I think we should step back and reexamine the surrounding code to find a fundamentally better way to address your use case

This sounds quite astonishing to me. For me organizing data in a hierarchical way is the most natural thing. Strangely enough, almost all models in the guide only consist of a few values. So the question is: how is complex data handled in Elm? How would a model for my requirement look like?

1 Like

Your suggested structure looks great! That’s how I’d do it too.

2 Likes

Forms are quite a complicated topic. I would recommend not thinking too much about type safety or else you will lose yourself in a deep deep rabbit hole. Its probably just fine starting with what you got and seeing how far it gets you.

For smaller forms, I would create update functions to help me update the nested structure.

type alias SmallForm =
    { firstName : String
    , secondName: String
    , address : List String
    , birthday : {year:Int,month:Int,day:Int}
    }

updateBirthday : {year:Int,month:Int,day:Int} -> SmallForm -> SmallForm

For bigger forms, I would start grouping the fields by type and put them into a dict:

type alias BigForm =
    { strings : Dict String String
    , ints : Dict String Int
    }

No matter which way you choose, you will probably want to validate the Form after submitting it. That’s usually where I convert it into something more useful like

type alias Contact =
  { name : Name
  , address : Address
  , birthday : Date
  }
type alias Name = 
  { firstName: String
  , secondName : String
  }
type alias Address =
 { country : String
 , street: String
 , door: String
 }
type alias Date =
 { year : Int
 , month : Month
 , day : Int
 }

Concerning Updating nested records, again.

Whenever I see multiple dots being used to access anything, methods nested deeply in an object hierarchy or fields nested deeply in a record or JavaScript object, I recall the Law of Demeter (video). It suggests a code smell in ALL languages, JavaScript, Go, Scheme, no matter how easy they make it to do.

That said, it doesn’t mean don’t do it, it just means that as the code size grows your application could become more difficult to maintain. You just have to decide on the tradeoffs you want to make.

Usually, whenever I encounter the need for nested data types, I usually take one of two ways:

Create a new data type

A lot of the sub-records are often identical, and you can create a new module with the new data type:

type alias Model =
    { formA : Form String String
    , formB : Form Int String
    , formC : Form Int Float
    }

type alias Form a b = { inputA : a, inputB : b }

Then, using separate functions, you can update the form types given that their inputs are of the correct type. For example:

{-| Update inputA if it is a String type. In the Model example above,
this function only works for model.formA
-}
updateFirstTextField : String -> Form String b -> Form String b
updateFirstTextField text form =
    { form | inputA = text }

-- Now use { data | formA = updateFirstField "foo" data.formA }

If you want separate entries per form, you can also create an Entry data type.

Extensible records

Usually, flattening down isn’t as bad as it may seem! You can easily have a record like:

type alias Model =
    { form1Entry1Input1 : String
    , form1Entry1Input2 : String
    , form1Entry2Input1 : Float
    , form1Entry2Input2 : Int
    -- etc
    }

Then, whenever you want to write a function that can only reach very specific parts of the record:

type Form1Entry1 a = { a | form1Entry1Input1 : String, form1Entry1Input2 : String }

{-| Yes, you can use Model as an input type for this function!
-}
viewFirstForm : Form1Entry1 a -> Html msg
viewFirstForm model =
    Html.div [] []
    -- TODO: Use model.form1Entry1Input1 and model.form1Entry1Input2 to create a form view

These names, of course, reek of a code smell, so generally using a List or Dict is more useful when you start getting large forms with many entries.

I think I have missed one aspect. Every input needs its particular kind of validation. And the nesting reflects the conjunctions for the validation. An entry is valid, if all nested inputs are valid. A form is valid if all nested entries are valid. A successor form becomes available, if its preceding form is valid. I do not think that flattening is a useful approach. And I have three state booleans: valid, invalid, unknown, because I can not spam the user with invalid-errors if he did not enter anything.

Dynamic scoping seems to be the exact opposite of the law of demeter. Changing a dynamic variable has an impact on the whole call stack not just the second or third level. But dynamic scoping is useful in some situations: EMACS: The Extensible, Customizable Display Editor - GNU Project - Free Software Foundation
You will find a similar argumentation in one of the Lambda papers:
https://dspace.mit.edu/bitstream/handle/1721.1/6094/AIM-453.pdf
Dynamic Scoping as a State-Decomposition Discipline (on page 43).

in that case you have two ways to go forward.

Either you use an existing package, like dilonkearns/elm-form that defines the architecture for you.

Or you write your own thing. But this has the possibility, that you go down a rabbit hole that will cost you a lot of time and maybe some of your sanity.
My personal recommendation, if you want to go this route is to don’t try to generalize and don’t try to be smart. Make a file for every form. If you want to create a multi-step form, than you should probably use elm-spa and create one Page per form.

I never mentioned anything about dynamic scoping/binding. And, I also agree that it has nothing to do with the Law of Demeter.

Aside

However, since you mentioned the topic, I will share a few things:

  1. I rather use a more controlled formed of dynamic binding like what Racket provides through parameterize.
  2. I implemented dynamic scoping/binding in this interpreter.
  3. I implemented parameterize (called setdynamic) in this interpreter.

I always thought that parameterize was a neat language based solution to what they do in React with createContext.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.