Html.fragment : List (Html msg) -> Html msg

hello !

This topic has been discussed already, I’m still in favour of it, as well as the Attributes equivalent (especially for styles):

let
    important =
        Html.Attributes.many
            [ Html.Attributes.style "background" "red"
            , Html.Attributes.style "color" "white"
            ]
in
Html.div
    [ important
    ]
    [ Html.text "very important!"
    ]

Would it not make sense in elm to call it Html.batch and Html.Attributes.batch ?

3 Likes

Yeah I think fragment might be too React-specific and not immediately obvious.

  • many sounds good (understandable) :+1:
  • batch has precedent in Cmd and Sub but I feel like it hints at non-ordered nature of the contents (perhaps I’m just interpolating that from Cmd and Sub) so it’s not my favorite for Html.
5 Likes

fromList would be better

1 Like

And maybe Html.concat and Html.Attributes.concat would be Even better :slight_smile:

@evancz Do you think this would be in principle an OK addition to the virtual-dom / Html / Svg API, or is it not worth it?

1 Like

(0) Great question and discussion. I found the examples really helpful.

(1) I think the solution might be to provide in Elm a list literal that produces both of

[ "Foo", "Bar", "Save" ]

[ "Foo", "Bar", "Save as new", "Save changes" ]

(2) This can be done in Python, by using its * unpacking operator.

>>> notSaved = [ 'Save' ]
>>> isSaved = [ 'Save as new', 'Save changes' ]

>>> [ 'Foo', 'Bar', *notSaved ]
['Foo', 'Bar', 'Save']

>>> [ 'Foo', 'Bar', *isSaved ]
['Foo', 'Bar', 'Save as new', 'Save changes']

(3) This change was introduced in Python 3.5 (2013) as a result of the analysis in

(4) The semantics of * avoids needless div elements in the Html.

(5) Python also has a ** unpacking operator for dictionaries.

(6) Does anyone know how to create a bold * in markdown?

2 Likes

Escaping the internal * should work: ** \* ** = *

Thanks. It does work, but you have to look closely. From ‘\***\***\***\***’ you get ‘****’.

(0) Here’s a summary of a useful private conversation with @Janiczek. I’ve made changes to what I said, when I think it’s an improvement.

(1) The starting point was a wish to have something like a list literal, but of variable length. And Html.fragment was proposed as a way of achieving this. The two arguments to div, button and so on are both lists. So any syntactic sugar here might be widely used.

(2) Literals are one way to construct a List. They are a convenient shortcut.

> 1 :: 2 :: 3 :: 4 :: (List.singleton 5)
[1,2,3,4,5] : List number
> [1, 2, 3, 4, 5]
[1,2,3,4,5] : List number

(2) By the way, constructing a record requires at least one literal for every record type, and another literal for every modification.

> type alias Rec = { a : Int , b : String }
> rec = Rec 2 "three"
{ a = 2, b = "three" } : Rec
> rec2 = { rec | a = 4 }
{ a = 4, b = "three" }  : { a : Int, b : String }

(3) The semantics of Python’s

[ a , b, * c, d ]

when written in Elm is

[ a, b ] ++ c ++ [ d ]

or in other words * c, is equivalent to the admittedly odd

] ++ c ++  [

(4) An alternative approach is

> c = [ 3, 4 ]
[3,4] : List number
> List.concat [ [ 1, 2 ], c, [ 5 ] ]
[1,2,3,4,5] : List number

(5) Either way, something close to Python’s * operator can be achieved in Elm via suitable literals.

(6) @Janiczek notes

* as a language syntax would certainly be more general than Html.Fragment . That can be its advantage and disadvantage (probably depending on who you ask).

(7) The Elm compiler does type implication. Also, every element in a list must have the same type. This would allow the compiler to imply the * operator in most list literals.

(8) For example, if we have the * operator then we would have

> b = [ 2, 3 ]
[2,3] : List number
> [ 1 , *b, 4 ]
[1,2,3,4] : List number

and implying the * operator would allow

> [ 1 , b, 4 ]
[1,2,3,4] : List number

(9) In this situation a literals such as

[ [ ], [ ] ]
[ [ 1 ] ], [ [ [ 2 ] ] ] ]

would be ambiguous.

(10) Implying the * operator doesn’t need a new ItemOrList type, even if the starred identifier comes from outside of a function. I suggest that the compiler adds the stars only when there’s one way to produce type-valid code.

(11) If that is done, then the ambiguous examples in (9) would produce Type Error. This is a lack of backwards compatibility. The code could be fixed by adding a type annotation.

(12) To conclude.

  1. Something similar to Html.Fragment can already be achieved, via List methods. Doing nothing is an option.

  2. The * operator might be a useful addition to List literals.

  3. The compiler implying the star is an alternative to adding the * operator.

(13) Finally, suppose the compiler implies the * operator. Then for example the value of

[0, 1, 2, aaa, 4]

could be any of

[0, 1, 2, 4]
[0, 1, 2, 3, 4]
[0, 1, 2, 7, 8, 9, 4]

depending on the value of aaa . This language feature might at first confuse beginners. Perhaps, even if it can be implied, we’d benefit from an explicit * operator.

smh

fragment : List (Html msg) -> Html msg
fragment =
    Html.span []

This sometimes works but a lot of the times it doesn’t.

If I have a css rule like .stack > * + * { margin-top: 1 rem;}, your solution will not do the proper thing.

I recall looking into batch for both Html and Attribute when I was implementing elm/virtual-dom, and I recall a few considerations that led me to not add it at the time:

  1. It’s would be quite difficult to implement efficiently. I do not recall the exact specifics, but I think it clashed with certain data structures I was relying on to get the good performance numbers. E.g. how should diffing work when you cannot align nodes one-to-one between frames? Should a fragment be flattened? But wouldn’t that cause catastrophic diffs for all subsequent nodes in an unkeyed setting? Yes! Okay, but then what do data structures look like to protect against that? Very extreme care would need to be taken so that adding such a thing would not push Elm out of the top contenders on perf. (Special care is needed to make sure a JS VM can specialize code, like keeping object shapes perfectly uniform, and I recall thinking that was part of what made this idea difficult.)

  2. I did a decent amount of work to make elm/virtual-dom as small as possible while still having great performance, and I suspect that resolving the diffing question above would come at a notable size cost if perf was maintained overall.

  3. I had a weird feeling about it. Would it make code more confusing and unpredictable in certain ways? Maybe it is that func : Html msg -> Html msg seems to be declaring unambiguously that a single node will be passed in, and with this addition, it’s not saying that anymore. I could not tell if that was bad or neutral. I just had a weird feeling about it.

If you are into this idea, I would recommend working through (1) and (2) to prove that perf is not degraded by such an addition and see what the size change is for Hello World, TodoMVC, etc. If it is degraded, I’d consider that a major mark against the idea. From there, it’s more plausible to have a discussion about (3) whether the “surprise factor” of a Html msg actually being 100 nodes during a debug session is an okay price to pay. Seems like a high cost compared to using ++ where the structure of the DOM would be explicit in the code. But like I said, I don’t know really. I just felt weird about it. I personally dropped that line of thought as I came to understand the implementation challenges :sweat_smile:

Anyway, I hope these recollections are helpful.

11 Likes

Bringing up somewhat related post(my earlier write-up):

Thinking out loud about none (essentially batch []) patterns.

1 Like

I had the same instinct as @Chadtech. React needs the concept of a Fragment for the same reason for a lot of syntax sugar in JavaScript/TypeScript: things are special, so they don’t compose together neatly.

If a JSX Element was just data, then you could compose it together more freely. I find that there is special syntax for a lot of similar things with JavaScript - the ternary conditional operator, conditional chaining syntax, etc. are all just because null (and undefined) are special things, rather than just another kind of data that composes together like anything else. That’s one of my favorite things about Elm - instead of optional chaining syntax, we have map functions which we can use for more than just Maybe values, which is a world of difference.

Similar to what @ymtszw mentioned, I find myself building these kinds of patterns often. Some of the patterns I often reach for:

buttonsView : { isSaved : Bool } -> Html Msg
buttonsView {isSaved} =
  let
    saveButtons : List Html Msg
    saveButtons =
        if isSaved then
            [ buttonView "Save as new" SaveAsNew
            , buttonView "Save changes" SaveChanges
            ]
        else
            [ buttonView "Save" Save ]
  in
  Html.div []
    (List.concat [
        [ buttonView "Foo" DoFoo
        , buttonView "Bar" DoBar
        ]
        , saveButtons
        ])

I also often use List.filterMap identity to remove Nothing from a list.

buttonsView : { isSaved : Bool } -> Html Msg
buttonsView {isSaved} =
  Html.div []
    ([ buttonView "Foo" DoFoo |> Just
    , buttonView "Bar" DoBar |> Just
    , buttonView "Save as new" SaveAsNew |> viewIf isSaved
    , buttonView "Save changes" SaveChanges |> viewIf isSaved
    , buttonView "Save" Save  |> viewIf (not isSaved)
    ]
    |> List.filterMap identity
    )

viewIf : Bool -> Html msg -> Maybe (Html msg)
viewIf condition html =
    if condition then
        Just html
    else
        Nothing

If we did anything to improve the situation, I would say the direction to go would be less special things because that’s what makes Elm lovely to work with. Instead of special things like Fragments, maybe some standard patterns that can be applied generally (not just for views), and perhaps a nice package to encapsulate that pattern in a way the community can build on as a standard convention.

10 Likes

Interesting discussion, initally I’m not keen on the initial fragment concept as I don’t really see the need in Elm. As mentioned in React this is needed because a component cannot return a list.

I usually concatenate list and that works fine.

let
    buttonWhenSaved = if isSaved then [ buttonView ... ] else []
    ...
in
div [] (baseButtons ++ buttonWhenSaved ++ buttonsWhenNotSaved)

However from this discussion there are two things I would love to see in Elm.

  1. NoOp values for Html and Attribute, instead of using text "" and class ""

  2. I think that being able to spread a list inside a list would be a great language feature:

let
   saveButtons =
     if saved then
        [ buttonView "Save as new" SaveAsNew
        , buttonView "Save changes" SaveChanges
        ]
    else
       [ buttonView "Save" Save ]
in
div []
  [
     buttonView "Foo" DoFoo
     , ...saveButtons
     , buttonView "Bar" DoBar
  ]

We can pretend…

It’s not 1st-party support and it uses the primitives you mentioned, but at least our code doesn’t need to care about those “implementation details” :man_shrugging:

2 Likes

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