Skip to content

Instantly share code, notes, and snippets.

@cpressey
Created September 4, 2020 13:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cpressey/eb1c0872e81c80ecfce56ec94f34086e to your computer and use it in GitHub Desktop.
Save cpressey/eb1c0872e81c80ecfce56ec94f34086e to your computer and use it in GitHub Desktop.
PipelineCombinators.hs
module PipelineCombinators where
import Data.Either
import qualified Data.Functor.Alt
import qualified Data.Function
import qualified Control.Monad
--
-- Combinators for pipelines. Each of these functions takes one or more
-- parameters and yields a combinator that is intended to map functions
-- of type (a -> Either e b) to other functions of type (a -> Either e b).
--
-- Many of the combinators exploit the fact that Either is a Monad
-- and an Alt, so do not strictly require that they work on Eithers.
--
-- The pipelines that can be formed look like this:
--
-- data & bend >=> spindle >=> mutilate <!>
-- coverWithPaint >=> foldIntoAirplane <!>
-- failure "Sorry, ran into a problem" & output
--
-- Thanks to dminoso, dolio, dibblego, merijn, and others on #haskell
-- on freenode for helpful answers to questions I had while writing this.
--
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--
-- To put data into a pipeline initially, you can use the reverse-application
-- operator & from Data.Function. In fact you can also use & to take the
-- result out of the pipeline at the end and feed it to another function.
--
-- The only problem is that the precedence doesn't play nicely with >=>,
-- and forces you to put the pipeline in parentheses. So we re-export it here
-- with lower precedence. If you don't like this you can hide this definition
---and use the one from Data.Function and add parentheses as necessary.
--
infixl 0 &
(&) :: a -> (a -> b) -> b
(&) = (Data.Function.&)
--
-- Given a value _v_, obtain a combinator that ignores what it receives in the
-- pipeline and always succeeds with _v_.
--
success :: Monad m => a -> t -> m a
success v _ = return v
--
-- Given an error _e_, obtain a combinator that ignores what it receives in the
-- pipeline and always fails with _e_.
--
failure :: a -> t -> Either a b
failure e _ = Left e
--
-- Given a transformer function _f_, obtain a combinator that, on success,
-- transforms the value by applying _f_ to it before returning it.
--
then_ :: Monad m => (t -> a) -> t -> m a
then_ f v = return $ f v
--
-- Given two combinators A and B, obtain a combinator which applies A, and
-- then, only if A succeeds, applies B to the result of A.
--
-- Originally I wrote it out like this, with concrete type instances:
--
originalAndThen :: (a -> Either e b) -> (b -> Either e c) -> (a -> Either e c)
f `originalAndThen` g =
(\a -> case f a of
Right b -> g b
Left msg -> Left msg)
--
-- ...but note that since Either is an instance of Monad, this is just (>=>).
-- We re-export it here.
--
infixr 1 >=>
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
(>=>) = (Control.Monad.>=>)
--
-- Given two combinators A and B, obtain a combinator which applies A to the
-- input, or else if A fails, applies B to the input.
--
--
-- Originally I wrote it out like this, with concrete type instances:
--
originalOrElse :: (a -> Either e b) -> (a -> Either e b) -> (a -> Either e b)
f `originalOrElse` g =
(\a -> case f a of
Right b -> Right b
Left _ -> g a)
--
-- I wanted a more general version, and found there are two issues here.
--
-- One is that, while MonadPlus would support choice like this, and Either
-- is a Monad, it is not a MonadPlus, because it is not Alternative, because
-- there is no sensible unique identity element for `empty` (in a sense, all
-- Left values are "empty".)
--
-- There is a version of Alternative which does not require identity --
-- Alt from semigrouoids. Either is an Alt. Well, let's use that then.
-- Then we can use <!> on two Either values, and get the leftmost
-- Right one.
--
-- The other issue, however, is that we don't want an operator that works
-- on Either values directly, but rather, on Either-producing functions.
--
-- But we can't make (a -> Either e b) into an instance of Alt for technical
-- reasons.
--
-- So we do the next best thing: write the function out, using <!> in it,
-- then provide our own infix operator that works like you'd expect.
--
-- We call our infix operator <!> because it is "morally" the same thing as
-- Alt's <!>. However, this could also lead to confusion, since it is a
-- different operator. If you want to use them both in the same module,
-- you'll need to qualify or rename one of them.
--
orElse :: Data.Functor.Alt.Alt f => (a -> f b) -> (a -> f b) -> (a -> f b)
f `orElse` g = (\x -> (f x) Data.Functor.Alt.<!> (g x))
infixl 3 <!>
(<!>) :: Data.Functor.Alt.Alt f => (a -> f b) -> (a -> f b) -> (a -> f b)
(<!>) = orElse
--
-- Given an error _e_ and a value _v_, obtain a combinator that transforms an
-- Either-producing function into a new Either-producing function with the
-- result flipped.
--
invert :: e -> b -> (c -> Either p q) -> (c -> Either e b)
invert e v f =
(\a -> case f a of
Right _ -> Left e
Left _ -> Right v)
--
-- Given a function which takes an accumulator and an item to an Either
-- value, and an initial accumulator, and a list of items, fold over
-- the list and return the modified accumulator. The folding stops
-- at the end of the list or the first Left result from the function,
-- whichever comes first.
--
-- Originally I wrote it out like this, with concrete type instances:
--
foldResult :: (a -> b -> Either e a) -> a -> [b] -> Either e a
foldResult fn acc [] = Right acc
foldResult fn acc (item:items) =
case (fn acc item) of
Right acc' -> foldResult fn acc' items
Left msg -> Left msg
--
-- But of course Either is a Monad and happily there is a fold for monads.
-- Which we re-export here.
--
foldM :: (Monad m, Foldable t) => (b -> a -> m b) -> b -> t a -> m b
foldM = Control.Monad.foldM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment