In case you were interested in full on phantom types, you can do it like this (not sure it’s worth it, but it typechecks):
module AWS.Phantom exposing (Thrown, NotThrown)
{- Not exposed -}
type Thrown
= Thrown
type NotThrown
= NotThrown
module AWS exposing (AwsError, PossibleErrors, foo, bar)
import Dict exposing (Dict)
import AWS.Phantom exposing (..)
type AwsError rec
= AwsError String
type alias PossibleErrors =
{ foo : NotThrown, bar : NotThrown, baz : NotThrown }
{-| These are the actual services. The type indicates which errors can be thrown -}
bar : Int -> Result (AwsError { a | baz : Thrown, bar : Thrown }) Int
bar n =
Debug.todo "not implemented"
foo : Int -> Result (AwsError { a | foo : Thrown, bar : Thrown }) Int
foo n =
Debug.todo "not implemented"
module AWS.ErrorHandler exposing (Handler, new, FooError, handleFoo, handleBar, handleBaz, handle)
import Dict exposing (Dict)
import AWS.Phantom exposing (..)
import AWS exposing (PossibleErrors)
{-| These make it nicer to read -}
type alias Handled =
Thrown
type alias NotHandled
= NotThrown
{-| This type encapsulates error handling logic -}
type Handler resultType errors
= Handler (Dict String (String -> resultType))
{-| The api here follows a builder pattern -}
newHandler : Handler b PossibleErrors
newHandler =
Handler Dict.empty
type FooError
= FooError
{-| Each possible error gets a function. These can each pass custom metadata to user code -}
handleFoo : (FooError -> b) -> Handler b { a | foo : NotHandled } -> Handler b { a | foo : Handled }
handleFoo fn (Handler handler) =
Handler (Dict.insert "FooError" (parseFooErrorStr >> fn) handler)
{-| They each track which errors are handled in the handler -}
handleBar : (FooError -> b) -> Handler b { a | bar : NotHandled } -> Handler b { a | bar : Handled }
handleBar fn (Handler handler) =
Debug.todo "not implemented"
handleBaz : (FooError -> b) -> Handler b { a | baz : NotHandled } -> Handler b { a | baz : Handled }
handleBaz fn (Handler handler) =
Debug.todo "not implemented"
{-| Finally we convert the type into a function that handles an error. At this point we assert that the two records need to match - we have provided a handler for each error. -}
handle : AwsError a ->Handler b a -> b
handle (AwsError error) (Handler handler) =
case Dict.get (parseErrorType error) handler of
Just fn ->
fn error
Nothing ->
Debug.todo "an excersize for the reader"
parseErrorType : String -> String
parseErrorType e =
Debug.todo "not implemented"
parseFooErrorStr : String -> FooError
parseFooErrorStr e =
Debug.todo "not implemented"
And then finally, an example of usage:
myTask =
case AWS.bar 2 |> Result.andThen (\n -> AWS.foo n) of
Ok n ->
n
Err err ->
ErrorHandler.newHandler
|> ErrorHandler.handleFoo (always 3)
|> ErrorHandler.handleBar (always 3)
|> ErrorHandler.handleBaz (always 3)
|> ErrorHandler.handle err