Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs.json

Large diffs are not rendered by default.

176 changes: 176 additions & 0 deletions src/Result/Extra.elm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Result.Extra exposing
( isOk, isErr, extract, unwrap, unpack, error, mapBoth, merge, join, partition, filter
, combine, combineMap, combineArray, combineMapArray, combineFirst, combineSecond, combineBoth, combineMapFirst, combineMapSecond, combineMapBoth
, filterArray, filterList, foldl, foldlArray, map2, map2Array
, andMap
, or, orLazy, orElseLazy, orElse
, toTask
Expand All @@ -19,6 +20,13 @@ module Result.Extra exposing
@docs combine, combineMap, combineArray, combineMapArray, combineFirst, combineSecond, combineBoth, combineMapFirst, combineMapSecond, combineMapBoth


# Containers (`List` / `Array`)

`Result`'s `andThen` behavior (monadic) is that it terminates execution immediately on first error. Projecting that into various operations on lists and arrays yields operations that terminate on first error.

@docs filterArray, filterList, foldl, foldlArray, map2, map2Array


# Applying

@docs andMap
Expand Down Expand Up @@ -163,6 +171,174 @@ combineHelp list acc =
Ok (List.reverse acc)


{-| Filter a list using a predicate function that can fail.
-}
filterList : (a -> Result error Bool) -> List a -> Result error (List a)
filterList pred =
let
go : List a -> List a -> Result error (List a)
go returnList current =
case current of
[] ->
Ok <| List.reverse returnList

head :: tail ->
case pred head of
Err err ->
Err err

Ok b ->
go
(if b then
head :: returnList

else
returnList
)
tail
in
go []


{-| Filter an array using a predicate function that can fail.
-}
filterArray : (a -> Result error Bool) -> Array.Array a -> Result error (Array.Array a)
filterArray pred arr =
let
go : List a -> Int -> Result error (Array.Array a)
go returnList index =
case Array.get index arr of
Nothing ->
Ok <| Array.fromList <| List.reverse returnList

Just head ->
case pred head of
Err err ->
Err err

Ok b ->
go
(if b then
head :: returnList

else
returnList
)
(index + 1)
in
go [] 0


{-| Map two lists together with a function that produces a Result and which terminates on the first `Err` in either list.

Known as `zipWithM` in Haskell / PureScript.

-}
map2 : (a -> b -> Result e c) -> List a -> List b -> Result e (List c)
map2 zip aList bList =
let
go : Result e (List c) -> List a -> List b -> Result e (List c)
go resultList a b =
case resultList of
Err _ ->
resultList

Ok list ->
case ( a, b ) of
( aHead :: aTail, bHead :: bTail ) ->
go (zip aHead bHead |> Result.map (\c -> c :: list)) aTail bTail

_ ->
Ok <| List.reverse list
in
go (Ok []) aList bList


{-| Map two arrays together with a function that produces a `Result` and which terminates on the first `Err` in either array.

Known as `zipWithM` in Haskell / PureScript.

-}
map2Array : (a -> b -> Result e c) -> Array.Array a -> Array.Array b -> Result e (Array.Array c)
map2Array zip aArray bArray =
let
go : Result e (Array.Array c) -> Int -> Result e (Array.Array c)
go resultArray index =
case resultArray of
Err _ ->
resultArray

Ok array ->
case ( Array.get index aArray, Array.get index bArray ) of
( Just aHead, Just bHead ) ->
go (zip aHead bHead |> Result.map (\c -> Array.push c array)) (index + 1)

_ ->
resultArray
in
go (Ok <| Array.empty) 0


{-| Like `List.foldl` but the step function produces a `Result` and the folding terminates the moment an `Err` is encountered. This function provides early termination like `List.Extra.stoppableFoldl` but has
the following benefits

- `Step state` used by `stoppableFoldl` supports only a **one** type for both the `Stop` and `Continue` cases. This function uses `Result` such that the terminated type (`Err terminated`) can differ from the continue type (`Ok state`).
- By using `Result` this function has improved ergonomics as `Result` is a rich type used throughout all of Elm. It is very likely you already have functions that return `Result` or return `Maybe` which can be converted easily to `Result` with `Result.fromMaybe`.

One can think of `foldl` as a functional for-loop where the `accumulator` is some local state that will be read and returned (likely updated) on each iteration over the container (`List`). By returning `Result` and
supporting early termination this is like a for-loop with a break or early exit condition.

Similar to `foldM :: (Foldable foldable, Monad m) => (b -> a -> m b) -> b -> foldable a -> m b` in Haskell / PureScript where the `m` is `Result` and `foldable` is `List`.

-}
foldl : (item -> state -> Result terminated state) -> state -> List item -> Result terminated state
foldl step initialState =
let
go : Result terminated state -> List item -> Result terminated state
go stateResult list =
case stateResult of
Err _ ->
stateResult

Ok state ->
case list of
[] ->
stateResult

head :: rest ->
go (step head state) rest
in
go (Ok initialState)


{-| Like `Array.foldl` except that the step function produces a `Result` and the folding terminates the moment an `Err` is encountered.

One can think of `foldl` as a functional for-loop where the `accumulator` is some local state that will be read and returned (likely updated) on each iteration over the container (`Array`). By returning `Result` and
supporting early termination this is like a for-loop with a break or early exit condition.

Similar to `foldM :: (Foldable foldable, Monad m) => (b -> a -> m b) -> b -> foldable a -> m b` in Haskell / PureScript where the `m` is `Result` and `foldable` is `Array`.

-}
foldlArray : (item -> state -> Result terminated state) -> state -> Array.Array item -> Result terminated state
foldlArray step initialState arr =
let
go : Result terminated state -> Int -> Result terminated state
go stateResult index =
case stateResult of
Err _ ->
stateResult

Ok currentState ->
case Array.get index arr of
Nothing ->
stateResult

Just head ->
go (step head currentState) (index + 1)
in
go (Ok initialState) 0


{-| Map a function producing results on a list
and combine those into a single result (holding a list).
Also known as `traverse` on lists.
Expand Down
144 changes: 144 additions & 0 deletions tests/ResultTests.elm
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module ResultTests exposing (all)

import Array
import Dict
import Expect
import Fuzz
import Result.Extra
Expand All @@ -16,9 +18,15 @@ import Result.Extra
, error
, extract
, filter
, filterArray
, filterList
, foldl
, foldlArray
, isErr
, isOk
, join
, map2
, map2Array
, mapBoth
, merge
, or
Expand All @@ -38,6 +46,7 @@ all =
describe "Result.Extra"
[ commonHelperTests
, combiningTests
, monadHelperTests
, applyingTests
, alternativesTests
, toTaskTests
Expand Down Expand Up @@ -178,6 +187,141 @@ combiningTests =
]


{-| The number of iterations to run to test for stack safety. Set this very high while testing but then lower before checking in to avoid slowing down all tests in CI.
-}
stackSafetyCount : Int
stackSafetyCount =
-- 500000
500


foldlTests : Test
foldlTests =
let
foldlTest foldl mkFromList =
let
peopleDict =
Dict.fromList
[ ( 1, { name = "Alice", isCool = True } )
, ( 2, { name = "Bob", isCool = False } )
, ( 3, { name = "Cherry", isCool = True } )
]

step current state =
Dict.get current peopleDict
|> Result.fromMaybe ("Unable to find person for " ++ String.fromInt current)
|> Result.map (\v -> { state | people = v.name :: state.people, allAreCool = state.allAreCool && v.isCool })

expect lst expected =
Expect.equal (foldl step { people = [], allAreCool = True } <| mkFromList lst) expected
in
[ test "empty" <| \_ -> expect [] <| Ok { people = [], allAreCool = True }
, test "singleton success" <| \_ -> expect [ 1 ] <| Ok { people = [ "Alice" ], allAreCool = True }
, test "singleton failure" <| \_ -> expect [ 11 ] <| Err "Unable to find person for 11"
, test "long success" <| \_ -> expect [ 1, 2, 3, 2 ] <| Ok { people = [ "Bob", "Cherry", "Bob", "Alice" ], allAreCool = False }
, test "long failure" <| \_ -> expect [ 1, 2, 3, 2, 1, 33 ] <| Err "Unable to find person for 33"
, test "extra long success to prove stack safety" <| \_ -> expect (List.repeat stackSafetyCount 1) <| Ok { people = List.repeat stackSafetyCount "Alice", allAreCool = True }
]
in
describe "foldl*"
[ describe "foldl" <| foldlTest foldl identity
, describe "foldlArray" <| foldlTest foldlArray Array.fromList
]


isEven : Int -> Bool
isEven n =
modBy 2 n == 0


map2Tests : Test
map2Tests =
let
errorMsg a b =
"Only even numbers can be added but we found " ++ String.fromInt a ++ " and " ++ String.fromInt b

map2Test map2 mkFromList =
let
zipper a b =
if isEven a && isEven b then
Ok <| a + b

else
Err <| errorMsg a b

expect a b expected =
Expect.equal (map2 zipper (mkFromList a) (mkFromList b)) <| Result.map mkFromList expected
in
[ test "empty" <| \_ -> expect [] [] <| Ok []
, test "one empty" <| \_ -> expect [] [ 0 ] <| Ok []
, test "other empty" <| \_ -> expect [ 0 ] [] <| Ok []
, test "singleton success" <| \_ -> expect [ 0 ] [ 0 ] <| Ok [ 0 ]
, test "singleton failure" <| \_ -> expect [ 0 ] [ 1 ] <| Err <| errorMsg 0 1
, test "long success" <| \_ -> expect [ 0, 10, 20, 30, 40, 50 ] [ 0, 2, 4, 6, 8, 10 ] <| Ok [ 0, 12, 24, 36, 48, 60 ]
, test "long failure" <| \_ -> expect [ 0, 10, 20, 30, 40, 50 ] [ 0, 2, 4, 6, 8, 11 ] <| Err <| errorMsg 50 11
, test "extra long success to prove stack safety" <|
\_ ->
let
list =
List.repeat stackSafetyCount 0
in
expect list list <| Ok list
]
in
describe "map2*"
[ describe "map2" <| map2Test map2 identity
, describe "map2Array" <| map2Test map2Array Array.fromList
]


filterContainerTests : Test
filterContainerTests =
let
over100 a =
"Only numbers <= 100. Found " ++ String.fromInt a

filterTest filter mkFromList =
let
pred a =
if a <= 100 then
Ok <| isEven a

else
Err <| over100 a

expect container expected =
Expect.equal (filter pred (mkFromList container)) <| Result.map mkFromList expected
in
[ test "empty" <| \_ -> expect [] <| Ok []
, test "singleton success" <| \_ -> expect [ 4 ] <| Ok [ 4 ]
, test "singleton success returning empty" <| \_ -> expect [ 5 ] <| Ok []
, test "singleton failure" <| \_ -> expect [ 102 ] <| Err <| over100 102
, test "long success" <| \_ -> expect [ 0, 2, 5, 4, 6, 8, 33, 10 ] <| Ok [ 0, 2, 4, 6, 8, 10 ]
, test "long failure" <| \_ -> expect [ 0, 10, 20, 30, 40, 105 ] <| Err <| over100 105
, test "extra long success to prove stack safety" <|
\_ ->
let
list =
List.repeat stackSafetyCount 0
in
expect list <| Ok list
]
in
describe "filter*"
[ describe "filterArray" <| filterTest filterArray Array.fromList
, describe "filterList" <| filterTest filterList identity
]


monadHelperTests : Test
monadHelperTests =
describe "List Helpers"
[ filterContainerTests
, foldlTests
, map2Tests
]


combineTests : Test
combineTests =
describe "combine"
Expand Down