elm-community / list-extra

Convenience functions for working with List.
http://package.elm-lang.org/packages/elm-community/list-extra/latest
MIT License
135 stars 58 forks source link

added List.foldlWhile function #89

Open AIRTucha opened 6 years ago

AIRTucha commented 6 years ago

List.foldlWhile function seems to be useful in two practical cases:

Accumulation of pixels in a grey scale image is a good example of the function use case.

    accomulateColor colorSum pixelColor =
        let
            newColorSum = colorSum + pixelColor
        in
            if newColorSum > 255 the
                Nothing
            else 
                Just newColorSum

    [10, 55, 63, 78]
        |> foldlWhile accomulateColor 0
Chadtech commented 6 years ago

Hey @AIRTucha ,

Could you elaborate about your use case a bit? In what kind of context would one be adding up gray scale values until they are 255?

foldWhile seems like a substitute for recursion to me. So either this function has to beat simple recursion in its usefulness, or it provides something different from recursion that I am not yet seeing.

AIRTucha commented 6 years ago

Hey @Chadtech, I am sorry for a long response, I was a little bit busy.

I might get you wrong, but all these functions are substitutes for recursion in some sense.

So, as I said before, there are two cases: 1) It is faster solution for

list 
        |> takeWhile someFunction1
        |> foldl someFunction2 someInitialValue

The performance is improved by: prevention of an intermediate creation and lower number of function calls. It should also reduce a pressure on GC.

2) You need to accumulate data from the list, but you want to stop if a certain condition is achieved. For example, you have a list of buckets and you wanna fill a tank. So, you do:

type alias Litre = Float

type alias Tank = 
  { level: Litre
  , volume: Litre
  }

add2Tank tank bucket =
  let
    newLevel = tank.level + bucket
  in
    if tank.volume > newLevel then
      Just newLevel
    else 
      Nothing

buckets: List Litre
buckets = 
  [ 1.5, 1.2, 2.0 ]

tank =                                                   -- { level: 4.7, volume: 5 }
  buckets
    |> add2Tank { level: 0.4, volume: 5 }

Of course, it can be easily done by recursion, but several times, I faced a situation when I want such a function.

Chadtech commented 6 years ago

Hey @AIRTucha , sorry for the delay in my reply. Heres a comparison as I see it, between using recursion and foldWhile

type alias Tank =
    { level : Float
    , volume : Float
    }

buckets : List Float
buckets =
    [ 1.5, 1.2, 2.0 ]

tank : Tank
tank =
    { level : 0.4 
    , volume : 5
    }

-- Recursion
fillTank : List Bucket -> Tank -> Tank
fillTank buckets tank =
    case buckets of
        first :: rest ->
            if first + tank.level > tank.volume then
                tank
            else
                fillTank { tank | level = tank.level + first } rest

        [] ->
            tank

-- foldWhile
fillTank : List Bucket -> Tank -> Tank
fillTank buckets tank =
    List.Extra.foldWhile addToTank tank buckets

addToTank : Bucket -> Tank -> Maybe Tank
addToTank bucket tank =
    if first + tank.level > tank.volume then
        Nothing
    else
        Just tank

I see the recursive technique at least as good at the foldWhile technique. Here are a few disadvantages that I see in the foldWhile technique:

0 The Maybe in the foldWhile technique hurts readability. Someone who is seeing the code for the first time might not understand the meaning of the Maybe, making it that much harder to grasp the purpose of the code at a glance. 1 The foldWhile technique requires List.Extra

pzp1997 commented 6 years ago

Great example @Chadtech. Just wanted to chime in that in addition to hurting readability, using Maybes in lieu of actual recursion most likely negatively affects performance.

AIRTucha commented 6 years ago

Wait a minute, you can eliminate foldl by the exact same way, can't you?

Ok, another possible case, actually, it was one of the reasons I wrote it here. So, I was working on some kind of parser. One of the key places in my project was folding over lines, parsing of each of them a combining them together. Parsing of each next line was based on a result previous one, so if it fails it has to stop and output the successful part.

I assume that there are some other case on which the same pattern can be applied.

Anyway, it is "extra", I used to think that the main idea here is to collect and experimental features and see if people are going to use them.