-- GHC wants us to remove `Err never` branches from case statements, because it -- knows we'll never end up in those branches. We like them though, because -- missing such a branch in a case statement looks like a problem and so is -- distracting. {-# OPTIONS_GHC -fno-warn-overlapping-patterns #-} -- | Tasks make it easy to describe asynchronous operations that may fail, like -- HTTP requests or writing to a database. module Task ( -- * Tasks Task, perform, attempt, -- * Chains andThen, succeed, fail, sequence, -- * Maps map, map2, map3, map4, map5, map6, -- * Errors onError, mapError, -- * Special (custom helpers not found in Elm) timeout, parallel, ) where import Basics import qualified Control.Concurrent.Async as Async import qualified Internal.Shortcut as Shortcut import List (List) import qualified List import Maybe (Maybe (..)) import Platform.Internal (Task) import qualified Platform.Internal as Internal import Result (Result (..)) import qualified System.Timeout import Prelude (IO) import qualified Prelude -- BASICS -- | Just having a @Task@ does not mean it is done. We must @perform@ the task: -- -- > import qualified Task -- > import qualified Platform -- > -- > main :: IO -- > main = -- > Task.perform Platform.silentHandler Time.now perform :: Internal.LogHandler -> Task Never a -> IO a perform output task = let onResult result = case result of -- If you remove this branch, consider also removing the -- -fno-warn-overlapping-patterns warning above. Err err -> never err Ok x -> x in attempt output task |> Shortcut.map onResult -- | This is very similar to perform except it can handle failures! attempt :: Internal.LogHandler -> Task x a -> IO (Result x a) attempt output task = let onResult result = Prelude.pure result in Internal._run task output |> Shortcut.andThen onResult -- | A task that succeeds immediately when run. It is usually used with -- @andThen@. You can use it like @map@ if you want: -- -- > import qualified Time -- > -- > timeInMillis : Task x Int -- > timeInMillis = -- > Time.now -- > |> andThen (\t -> succeed (Time.posixToMillis t)) succeed :: a -> Task x a succeed a = Internal.Task <| \_ -> Prelude.pure (Ok a) -- | A task that fails immediately when run. Like with @succeed@, this can be -- used with @andThen@ to check on the outcome of another task. -- -- > type Error = NotFound -- > -- > notFound : Task Error a -- > notFound = -- > fail NotFound fail :: x -> Task x a fail x = Internal.Task <| \_ -> Prelude.pure (Err x) -- MAPS -- | Transform a task. Maybe you want to figure out what time it will be in one -- hour: -- -- > import Task exposing (Task) -- > import qualified Time -- > -- > timeInOneHour : Task x Time.Posix -- > timeInOneHour = -- > Task.map addAnHour Time.now -- > -- > addAnHour : Time.Posix -> Time.Posix -- > addAnHour time = -- > Time.millisToPosix (Time.posixToMillis time + 60 * 60 * 1000) map :: (a -> b) -> Task x a -> Task x b map = Shortcut.map -- | Put the results of two tasks together. For example, if we wanted to know -- the current month, we could ask: -- -- > import qualified Task exposing (Task) -- > import qualified Time -- > -- > getMonth : Task x Int -- > getMonth = -- > Task.map2 Time.toMonth Time.here Time.now -- -- __Note:__ Say we were doing HTTP requests instead. @map2@ does each task in -- order, so it would try the first request and only continue after it succeeds. -- If it fails, the whole thing fails! map2 :: (a -> b -> result) -> Task x a -> Task x b -> Task x result map2 = Shortcut.map2 -- | map3 :: (a -> b -> c -> result) -> Task x a -> Task x b -> Task x c -> Task x result map3 = Shortcut.map3 -- | map4 :: (a -> b -> c -> d -> result) -> Task x a -> Task x b -> Task x c -> Task x d -> Task x result map4 = Shortcut.map4 -- | map5 :: (a -> b -> c -> d -> e -> result) -> Task x a -> Task x b -> Task x c -> Task x d -> Task x e -> Task x result map5 = Shortcut.map5 -- | map6 :: (a -> b -> c -> d -> e -> f -> result) -> Task x a -> Task x b -> Task x c -> Task x d -> Task x e -> Task x f -> Task x result map6 = Shortcut.map6 -- | Chain together a task and a callback. The first task will run, and if it is -- successful, you give the result to the callback resulting in another task. This -- task then gets run. We could use this to make a task that resolves an hour from -- now: -- -- > import qualified Time -- > import qualified Process -- > -- > timeInOneHour : Task x Time.Posix -- > timeInOneHour = -- > Process.sleep (60 * 60 * 1000) -- > |> andThen (\_ -> Time.now) -- -- First the process sleeps for an hour __and then__ it tells us what time it is. andThen :: (a -> Task x b) -> Task x a -> Task x b andThen = Shortcut.andThen -- | Start with a list of tasks, and turn them into a single task that returns a -- list. The tasks will be run in order one-by-one and if any task fails the whole -- sequence fails. -- -- > sequence [ succeed 1, succeed 2 ] == succeed [ 1, 2 ] sequence :: List (Task x a) -> Task x (List a) sequence tasks = List.foldr (Shortcut.map2 (:)) (succeed []) tasks -- | Start with a list of tasks, and turn them into a single task that returns a -- list. The tasks will be run in parallel and if any task fails the whole -- parallel call fails. -- -- > parallel [ succeed 1, succeed 2 ] == succeed [ 1, 2 ] parallel :: List (Task x a) -> Task x (List a) parallel tasks = Internal.Task ( \handler -> Async.forConcurrently tasks (\task -> Internal._run task handler) |> Shortcut.map Prelude.sequence ) -- | Recover from a failure in a task. If the given task fails, we use the -- callback to recover. -- -- > fail "file not found" -- > |> onError (\msg -> succeed 42) -- > -- succeed 42 -- > -- > succeed 9 -- > |> onError (\msg -> succeed 42) -- > -- succeed 9 onError :: (x -> Task y a) -> Task x a -> Task y a onError func task = Internal.Task <| \key -> let onResult result = case result of Ok ok -> Prelude.pure (Ok ok) Err err -> Internal._run (func err) key in Internal._run task key |> Shortcut.andThen onResult -- | Transform the error value. This can be useful if you need a bunch of error -- types to match up. -- -- > type Error -- > = Http Http.Error -- > | WebGL WebGL.Error -- > -- > getResources : Task Error Resource -- > getResources = -- > sequence -- > [ mapError Http serverTask -- > , mapError WebGL textureTask -- > ] mapError :: (x -> y) -> Task x a -> Task y a mapError func task = task |> onError (fail << func) -- | Run a task. If it doesn't complete within the given number of milliseconds -- then fail it with the provided error. -- -- > Process.sleep 2000 -- > |> timeout 1000 "overslept!" timeout :: Float -> err -> Task err a -> Task err a timeout duration err task = Internal.Task ( \handler -> do maybeResult <- System.Timeout.timeout (Prelude.round (1000 * duration)) (Internal._run task handler) case maybeResult of Just result -> Prelude.pure result Nothing -> Prelude.pure (Err err) )