Created
September 4, 2020 13:33
-
-
Save cpressey/eb1c0872e81c80ecfce56ec94f34086e to your computer and use it in GitHub Desktop.
PipelineCombinators.hs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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