git @ Cat's Eye Technologies The-Dipple / master haskell / PipelineCombinators.hs
master

Tree @master (Download .tar.gz)

PipelineCombinators.hs @masterraw · history · blame

module PipelineCombinators where

-- SPDX-FileCopyrightText: Chris Pressey, the original author of this work, has dedicated it to the public domain.
-- For more information, please refer to <https://unlicense.org/>
-- SPDX-License-Identifier: Unlicense

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