Search code examples
functional-programmingelm

How do I split a list in to four lists in elm?


I've got a list of items that have to be rendered. I have a function called viewItem that can render one item. I do a simple List.map viewItem items and now I have a list of items that can be displayed.

My view has four columns. How can I split this list into four lists that contain all of the elements from my original list?

This is how I'm doing it now, but there has to be something I'm missing. I want to be able to split it into five columns or even six without having to write col4 = ... and col5 = ... every time.

splitColumns : Int -> Array a -> Array (List a)
splitColumns cnum xs =
    let
        ixdList =
            Array.toIndexedList xs
    in
    List.filterMap
        (\a ->
            if modBy 4 (Tuple.first a) == cnum then
                Just (Tuple.second a)

            else
                Nothing
        )
        ixdList


viewItems : Array Item -> Html msg
viewItems items =
    let
        itemsHtml =
            Array.map viewItem items

        col0 =
            splitColumns 0 itemsHtml

        col1 =
            splitColumns 1 itemsHtml

        col2 =
            splitColumns 2 itemsHtml

        col3 =
            splitColumns 3 itemsHtml
    in
    main_
        [ class "section" ]
        [ Html.div
            [ class "container" ]
            [ Html.div
                [ class "columns" ]
                [ Html.div
                    [ class "column" ]
                    col0
                , Html.div
                    [ class "column" ]
                    col1
                , Html.div
                    [ class "column" ]
                    col2
                , Html.div
                    [ class "column" ]
                    col3
                ]
            ]
        ]

Solution

  • You could rewrite your current approach as a fold that only does one pass like this:

    cols : List a -> { col0 : List a, col1 : List a, col2 : List a, col3 : List a }
    cols list =
        list
            |> List.foldl
                (\x ( i, cols ) ->
                    case modBy 4 i of
                        0 ->
                            ( i + 1, { cols | col0 = x :: cols.col0 } )
    
                        1 ->
                            ( i + 1, { cols | col1 = x :: cols.col1 } )
    
                        2 ->
                            ( i + 1, { cols | col2 = x :: cols.col2 } )
    
                        3 ->
                            ( i + 1, { cols | col3 = x :: cols.col3 } )
    
                        _ ->
                            ( i + 1, cols )
                )
                ( 0, { col0 = [], col1 = [], col2 = [], col3 = [] } )
            |> Tuple.second
    

    This also keeps track of the index internally, so it doesn't require that you give it an indexed list, but it's still hard-coded for four columns. If we want to be able to use it with an arbitrary number of columns, we have to use a data structure that can hold an arbitrary number of items in sequence. An array is perfect for this, allowing us to update it with an index computed using modBy:

    cols : Int -> List a -> List (List a)
    cols n list =
        list
            |> List.foldl
                (\x ( i, cols ) ->
                    let
                        index =
                            modBy n i
    
                        tail =
                            cols |> Array.get index |> Maybe.withDefault []
                    in
                    ( i + 1, Array.set index (x :: tail) cols )
                )
                ( 0, Array.repeat n [] )
            |> Tuple.second
            |> Array.toList
    

    We can then use List.map in the view function to render them:

    viewItems : Array Item -> Html msg
    viewItems items =
        let
            itemsHtml =
                Array.map viewItem items
                    |> Array.toList
        in
        main_
            [ class "section" ]
            [ Html.div
                [ class "container" ]
                [ Html.div
                    [ class "columns" ]
                    (cols 4 itemsHtml |> List.map (Html.div [ class "column" ]))
                ]
            ]