Last active
January 17, 2020 16:41
-
-
Save danidiaz/2157e68f5d4967e468a9d062d4476adf to your computer and use it in GitHub Desktop.
A GADT for chains of functions, that remembers the types of the intermediate steps using a type-level list. https://www.reddit.com/r/haskell/comments/ei5kek/monthly_hask_anything_january_2020/fcpeppz/
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
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE GADTs #-} | |
module Pipy where | |
import Data.Kind (Type) | |
import GHC.TypeLits | |
import Data.Char (isAlpha) | |
-- GADT containing a chain of functions, and which remembers the types of each internal step. | |
data Pipeline (start :: Type) (middle :: [Type]) (end :: Type) where | |
-- A pipeline built from a plain function doesn't have internal steps | |
Simple :: (start -> end) -> Pipeline start '[] end | |
Composite :: (start -> head) -> Pipeline head middle end -> Pipeline start (head : middle) end | |
-- Forget about the internal steps, give me a plain function from start to end | |
runPipeline :: Pipeline start middle end -> start -> end | |
runPipeline (Simple f) = f | |
runPipeline (Composite f rest) = runPipeline rest . f | |
-- "tail" for pipelines is easy | |
tailPipeline :: Pipeline start (head : middle) end -> Pipeline head middle end | |
tailPipeline (Composite _ rest) = rest | |
-- "init" for pipelines is a bit harder, and in order to define it we need this | |
-- typeclass. | |
-- | |
-- Notice that the instances hang on the type-level lists that | |
-- represent the internal steps of the pipeline. | |
-- | |
-- Notice also that there's no instance for the empty list '[], as it wouldn't | |
-- make sense. | |
class Snoc (xs :: [Type]) where | |
type Init xs :: [Type] -- all elements of the argument list minus the last | |
type Last xs :: Type | |
initPipeline :: Pipeline start xs end -> Pipeline start (Init xs) (Last xs) | |
-- Pipelines with only one internal step are the base case. | |
instance Snoc '[x] where | |
type Init '[x] = '[] | |
type Last '[x] = x | |
initPipeline (Composite start _) = Simple start | |
-- Instance for pipelines with more than one internal step. | |
instance Snoc (y : zs) => Snoc (x : y : zs) where | |
type Init (x : y : zs) = x : Init (y : zs) | |
type Last (x : y : zs) = Last (y : zs) | |
initPipeline (Composite start rest) = Composite start (initPipeline rest) | |
-- Examples | |
example1 :: Pipeline String '[Char] Bool | |
example1 = Composite head (Simple isAlpha) | |
example2 :: Pipeline Char '[] Bool | |
example2 = tailPipeline example1 | |
example3 :: Pipeline String '[] Char | |
example3 = initPipeline example1 | |
runExample1 :: String -> Bool | |
runExample1 = runPipeline example1 | |
main :: IO () | |
main = pure () | |
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
-- (This gist depends on the "red-black-record" package. http://hackage.haskell.org/package/red-black-record) | |
-- This version defines pipelines with the help of a type-level graph. | |
-- We can edit individual nodes of the graph before generating the pipeline. | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE GADTs #-} | |
{-# LANGUAGE DataKinds #-} | |
{-# LANGUAGE PolyKinds #-} | |
{-# LANGUAGE TypeFamilies #-} | |
{-# LANGUAGE MultiParamTypeClasses #-} | |
{-# LANGUAGE ScopedTypeVariables #-} | |
{-# LANGUAGE TypeApplications #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE UndecidableInstances #-} | |
module Pipy where | |
import Data.Kind (Type) | |
import Data.Type.Equality (type (==)) | |
import Data.Proxy | |
import Data.RBR (Map,Record,Key,Value,project,insert,unit,delete,setField) -- from "red-black-record" | |
import GHC.TypeLits | |
-- To be used lifted, as a kind. | |
-- The 'c' variable is a pointer to the next node in the pipeline, if it exists. | |
data Segment a b c = Segment a b (Maybe c) | |
-- A type-level graph | |
type Graph = Map Symbol (Segment Type Type Symbol) | |
type family SegmentFunction (s :: Segment Type Type n) where | |
SegmentFunction ('Segment a b _) = a -> b | |
-- Helper newtype. For a given segment, wrap the function associated to it. | |
newtype Y (s :: Segment Type Type x) = Y {unY :: SegmentFunction s} | |
-- Extract a pipeline by entereing the graph at a given node and following the | |
-- pointers to other nodes. | |
class ExtractPipeline (k :: Symbol) (g :: Graph) where | |
type Start k g :: Type | |
type End k g :: Type | |
-- The Record is a datatype indexed by the graph, where each field comes | |
-- wrapped in a an Y parameterized by a segment. | |
-- In other words: it's a record of functions. | |
extractPipeline :: Proxy k -> Record Y g -> Start k g -> End k g | |
-- Auxiliary typeclass to avoid the need for OverlappinInstances. | |
class ExtractPipeline_ (s :: Segment Type Type Symbol) (g :: Graph) where | |
type Start_ s g :: Type | |
type End_ s g :: Type | |
extractPipeline_ :: Proxy s -> Y s -> Record Y g -> Start_ s g -> End_ s g | |
-- The main class delegates on the auxiliary one | |
instance (Key k g, ExtractPipeline_ (Value k g) g) => ExtractPipeline k g where | |
type Start k g = Start_ (Value k g) g | |
type End k g = End_ (Value k g) g | |
extractPipeline _ r = extractPipeline_ (Proxy @(Value k g)) (project @k r) r | |
-- Simplest case: extract a piepline from a final node. | |
instance ExtractPipeline_ ('Segment a b 'Nothing) g where | |
type Start_ ('Segment a b Nothing) g = a | |
type End_ ('Segment a b Nothing) g = b | |
extractPipeline_ _ (Y f) _ = f | |
-- Recursive case: extract a piepline from a node with a "next" pointer. | |
-- We check that the start type of the next segment is equal to the end type of the current one. | |
instance (ExtractPipeline next g, Start next g ~ b) => ExtractPipeline_ ('Segment a b ('Just next)) g where | |
type Start_ ('Segment a b ('Just next)) g = a | |
type End_ ('Segment a b ('Just next)) g = End next g | |
extractPipeline_ _ (Y f) r = extractPipeline (Proxy @next) r . f | |
main :: IO () | |
main = do | |
let r = insert @"start" @('Segment [Int] Int (Just "middle")) (Y head) | |
. insert @"middle" @('Segment Int Int (Just "end")) (Y succ) | |
. insert @"end" @('Segment Int String 'Nothing) (Y show) | |
$ unit | |
print $ extractPipeline (Proxy @"start") r [1,2,3] | |
print $ extractPipeline (Proxy @"middle") r 2 | |
print $ extractPipeline (Proxy @"end") r 1 | |
-- | |
-- changing the function for a node is relatively easy | |
let r2 = setField @"middle" (Y (succ . succ)) r | |
print $ extractPipeline (Proxy @"middle") r2 2 | |
-- | |
-- Changing a pointer to a "next node" is more involved. | |
-- We must delete and reinsert the field in the record | |
-- because a limitation in red-black-record. | |
-- Much boilerplate :( | |
let Y f = project @"start" r | |
r3 = insert @"start" @('Segment [Int] Int (Just "end")) (Y f) | |
. delete @"start" @('Segment [Int] Int (Just "middle")) | |
$ r | |
print $ extractPipeline (Proxy @"start") r3 [1,2,3] | |
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
-- (This gist depends on the "red-black-record" package. http://hackage.haskell.org/package/red-black-record) | |
-- Inspired by https://www.reddit.com/r/haskell/comments/ei5kek/monthly_hask_anything_january_2020/fcpeppz/ | |
-- This version defines pipelines of functions with the help of a type-level graph. | |
-- We can edit individual nodes of the graph before extracting the pipeline. | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE GADTs #-} | |
{-# LANGUAGE DataKinds #-} | |
{-# LANGUAGE PolyKinds #-} | |
{-# LANGUAGE TypeFamilies #-} | |
{-# LANGUAGE MultiParamTypeClasses #-} | |
{-# LANGUAGE ScopedTypeVariables #-} | |
{-# LANGUAGE TypeApplications #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE UndecidableInstances #-} | |
{-# LANGUAGE TypeOperators #-} | |
{-# LANGUAGE AllowAmbiguousTypes #-} | |
module Pipy where | |
import Data.Kind (Type) | |
import Data.Type.Equality (type (==)) | |
import Data.Proxy | |
-- from "red-black-record" | |
import Data.RBR (Map -- type-level map | |
,Empty -- the empty map | |
,FromList -- construct a type-level map from type-level list of tuples | |
,Key(Value) -- is a key in the type-level map? | |
,Insertable(Insert) -- can we insert this k/v combination into the typelevel map? | |
,Deletable(Delete) -- can we delete this k/v combination into the typelevel map? | |
,Record -- record indexed by a type-level map | |
,unit -- trivial record without fileds | |
,project -- get field from record | |
,insert -- add field to record | |
,setField) -- modify a field in the record without changing types) | |
import GHC.TypeLits | |
-- We have two type-level maps, with the same keys. | |
-- One determines the types of each pipeline segment. | |
-- The other determines the links between segments. | |
-- Formerly it was a single map, but it was more cumbersome that way. | |
-- But having two maps will make compilation slower :( | |
-- To be used lifted, as a kind. | |
-- Used to constrain the Record to contain only functions, and not arbitrary types. | |
data Segment a b = Segment a b | |
-- A type-level graph which defined connections between nodes. Purely type-level info. | |
type Graph = Map Symbol (Maybe Symbol) | |
-- A type-level map of node info. Will index a term-level record carrying the | |
-- actual functions of the pipeline. | |
type Nodes = Map Symbol (Segment Type Type) | |
-- Get the function type which corresponds to the segment | |
type family SegmentFunction (s :: Segment Type Type) :: Type where | |
SegmentFunction ('Segment a b) = a -> b | |
-- Helper newtype. For a given segment, wrap the function associated to it. | |
newtype Y (s :: Segment Type Type) = Y {unY :: SegmentFunction s} | |
-- Extract a pipeline by entering the graph at a given node and following the | |
-- pointers to other nodes. | |
class ExtractPipeline (k :: Symbol) (g :: Graph) (n :: Nodes) where | |
type Start k g n:: Type | |
type End k g n :: Type | |
-- The Record is a datatype indexed by the node map. Each field comes | |
-- wrapped in a an Y parameterized by a segment. In other words: it's a | |
-- record of functions. | |
extractPipeline :: Record Y n -> Start k g n -> End k g n | |
-- Auxiliary typeclass to avoid the need for OverlappingInstances. | |
class ExtractPipeline_ (s :: Segment Type Type) (next :: Maybe Symbol) (g :: Graph) (n :: Nodes) where | |
type Start_ s next g n :: Type | |
type End_ s next g n :: Type | |
extractPipeline_ :: Y s -> Record Y n -> Start_ s next g n -> End_ s next g n | |
-- The main class delegates on the auxiliary one | |
instance (Key k g, Key k n, ExtractPipeline_ (Value k n) (Value k g) g n) => ExtractPipeline k g n where | |
type Start k g n = Start_ (Value k n) (Value k g) g n | |
type End k g n = End_ (Value k n) (Value k g) g n | |
extractPipeline r = extractPipeline_ @(Value k n) @(Value k g) @g @n (project @k r) r | |
-- Simplest case: extract a pipeline from a final node. | |
instance ExtractPipeline_ ('Segment a b) 'Nothing g n where | |
type Start_ ('Segment a b) Nothing g n = a | |
type End_ ('Segment a b) Nothing g n = b -- b is the final type because there are no further nodes | |
extractPipeline_ (Y f) _ = f | |
-- Recursive case: extract a pipeline from a node with a "next" pointer. | |
-- We check that the start type of the next segment is equal to the end type of the current one. | |
instance (ExtractPipeline next g n, Start next g n ~ b) => ExtractPipeline_ ('Segment a b) (Just next) g n where | |
type Start_ ('Segment a b) ('Just next) g n = a | |
type End_ ('Segment a b ) ('Just next) g n = End next g n | |
extractPipeline_ (Y f) r = extractPipeline @next @g @n r . f | |
-- helper for adding a segment to the record of functions | |
putfun :: forall k a b n . Insertable k ('Segment a b) n => (a -> b) -> Record Y n -> Record Y (Insert k ('Segment a b) n) | |
putfun f = insert @k @('Segment a b) (Y f) | |
-- modify a function in the pipeline, but keeping the same type | |
setfun :: forall k a b n . (Key k n, Value k n ~ 'Segment a b) => (a -> b) -> Record Y n -> Record Y n | |
setfun f = setField @k (Y f) | |
-- Helper to make constructing linear paths easier | |
type family FromPath (path :: [Symbol]) :: Map Symbol (Maybe Symbol) where | |
FromPath '[x] = Insert x Nothing Empty | |
FromPath (x : y : zs) = Insert x (Just y) (FromPath (y : zs)) | |
-- Helpers to modify paths in the type-level graph. | |
-- Delete helper that uses Key to avoid having to mention the value | |
type Delete' (k :: Symbol) (t :: Map Symbol q) = Delete k (Value k t) t | |
type Cut (key :: Symbol) (g :: Graph) = Insert key Nothing (Delete' key g) | |
type Extend (key :: Symbol) (new :: Symbol) (g :: Graph) = Insert key (Just new) (Delete key Nothing g) | |
type Redirect (key :: Symbol) (new :: Symbol) (g :: Graph) = Insert key (Just new) (Delete' key g) | |
-- Example | |
-- type Graph01 = FromList [ '("start", Just "middle") | |
-- , '("middle", Just "end") | |
-- , '("end", Nothing) | |
-- ] | |
type Graph01 = FromPath [ "start", "middle", "end" ] | |
main :: IO () | |
main = do | |
let r = putfun @"start" @[Int] @Int head | |
. putfun @"middle" @Int @Int succ | |
. putfun @"end" @Int @String show | |
$ unit | |
print $ extractPipeline @"start" @Graph01 r [1,2,3] | |
print $ extractPipeline @"middle" @Graph01 r 2 | |
print $ extractPipeline @"end" @Graph01 r 1 | |
-- starting from the middle | |
print $ extractPipeline @"middle" @Graph01 r 2 | |
-- cutting the end | |
print $ extractPipeline @"start" @(Cut "middle" Graph01) r [1,2,3] | |
-- changing the function for a node is relatively easy | |
print $ extractPipeline @"start" @Graph01 (setfun @"middle" (succ . succ) r) [1,2,3] | |
-- cutting the pipeline earlier | |
print $ extractPipeline @"start" @(Redirect "start" "end" Graph01) r [1,2,3] |
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
-- Inspired by Plucking Constraints https://www.parsonsmatt.org/2020/01/03/plucking_constraints.html | |
-- How to avoid the bolierplate? | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE DataKinds #-} | |
{-# LANGUAGE KindSignatures #-} | |
{-# LANGUAGE MultiParamTypeClasses #-} | |
{-# LANGUAGE ScopedTypeVariables #-} | |
{-# LANGUAGE TypeApplications #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE UndecidableInstances #-} | |
{-# LANGUAGE AllowAmbiguousTypes #-} | |
{-# LANGUAGE TypeOperators #-} | |
module Plucky where | |
import Data.Type.Equality (type (==)) | |
class AsError e es where | |
liftError :: e -> es | |
isError :: es -> Maybe e | |
data Pluck e es = | |
This e | |
| Other es | |
-- Auxiliary class to avoid OverlappingInstances. AsError delegates on this. | |
-- It has an additional Bool argument to distinguish if the head matches. | |
-- Maybe using OverlappingInstances would be easier? | |
class AsError_ (b :: Bool) e es where | |
liftError_ :: e -> es | |
isError_ :: es -> Maybe e | |
-- Yes, it matches the head | |
instance AsError_ True e (Pluck e es) where | |
liftError_ = This | |
isError_ (This e) = Just e | |
isError_ (Other es) = Nothing | |
-- Nah, look elsewhere | |
instance AsError e' es => AsError_ False e' (Pluck e es) where | |
liftError_ e' = Other (liftError @e' @es e') | |
isError_ (This _) = Nothing | |
isError_ (Other es) = isError @e' @es es | |
-- AsError delegates on AsError_ | |
instance AsError_ (e' == e) e' (Pluck e es) => AsError e' (Pluck e es) where | |
liftError = liftError_ @(e' == e) | |
isError = isError_ @(e' == e) | |
-- | |
-- | |
-- | |
data DbError = DbError | |
data HttpError = HttpError | |
data FileError = FileError | |
program | |
:: (AsError HttpError e, AsError DbError e) | |
=> Either e () | |
program = do | |
Left (liftError HttpError) | |
Left (liftError DbError) | |
handleLeft :: forall err err' a . Either err a -> (err -> Either err' a) -> Either err' a | |
handleLeft (Right r) _ = Right r | |
handleLeft (Left l) f = f l | |
program'' :: forall e. AsError HttpError e => Either e () | |
program'' = | |
handleLeft @(Pluck DbError e) program $ \err -> | |
case err of | |
This dbError -> | |
Right () | |
Other dbOther -> | |
Left dbOther | |
-- | |
-- helper class that could be useful when choosing a concrete error carrier | |
data ErrorABC a b c = | |
ErrorA a | |
| ErrorB b | |
| ErrorC c | |
instance AsError a (ErrorABC a b c) where | |
liftError = ErrorA | |
isError (ErrorA a) = Just a | |
isError _ = Nothing | |
instance AsError b (ErrorABC a b c) where | |
liftError = ErrorB | |
isError (ErrorB b) = Just b | |
isError _ = Nothing | |
instance AsError c (ErrorABC a b c) where | |
liftError = ErrorC | |
isError (ErrorC c) = Just c | |
isError _ = Nothing | |
foo1,foo2,foo3 :: ErrorABC Int Bool Char | |
foo1 = liftError (5::Int) | |
foo2 = liftError True | |
foo3 = liftError 'a' | |
main :: IO () | |
main = do | |
return () |
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
-- A failed attempt at automating "plucking constraints" for errors: | |
-- https://www.reddit.com/r/haskell/comments/ejjnss/plucking_constraints/ | |
-- It bogged down because my type-level maps are really dumb. | |
-- "Your type level map has a key A. You deleted key B. What? You expect me to know that the map still has A?" | |
-- This gist depends on the "red-black-record" package. | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE GADTs #-} | |
{-# LANGUAGE DataKinds #-} | |
{-# LANGUAGE PolyKinds #-} | |
{-# LANGUAGE TypeFamilies #-} | |
{-# LANGUAGE MultiParamTypeClasses #-} | |
{-# LANGUAGE ScopedTypeVariables #-} | |
{-# LANGUAGE TypeApplications #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE UndecidableInstances #-} | |
{-# LANGUAGE ConstraintKinds #-} | |
{-# LANGUAGE AllowAmbiguousTypes #-} | |
module Pipy where | |
import Data.Kind (Type) | |
import Data.Type.Equality (type (==)) | |
import Data.Proxy | |
import Data.RBR (Map,Record,Key,Value,project,insert,unit,delete,setField,Deletable(..),Insertable(..),I(..),Variant,winnowI) | |
import GHC.TypeLits | |
type HasError (name :: Symbol) (error :: Type) (t :: Map Symbol Type) = (Key name t, Value name t ~ error) | |
type LacksError (name :: Symbol) (error :: Type) (t :: Map Symbol Type) (t' :: Map Symbol Type) = (HasError name error t', Deletable name error t', Delete name error t' ~ t) | |
data ErrorA = ErrorA | |
data ErrorB = ErrorB | |
data ErrorC = ErrorC | |
funcErrorBig :: forall g . (HasError "A" ErrorA g, HasError "B" ErrorB g, HasError "C" ErrorC g) => Either (Variant I g) () | |
funcErrorBig = undefined | |
-- How to prove: if a map contains a, b!=a, and we delete b from the map, a is still present. | |
funcErrorSmall :: forall t t'. (HasError "A" ErrorA t, | |
HasError "A" ErrorA t', | |
HasError "B" ErrorB t, | |
HasError "B" ErrorB t', | |
LacksError "C" ErrorC t t') | |
=> Either (Variant I t) () | |
funcErrorSmall = | |
case funcErrorBig @t' of | |
Right () -> Right () | |
Left e -> case winnowI @"C" @ErrorC @t' e of | |
Left smaller -> Left smaller | |
Right w -> Right () | |
main :: IO () | |
main = do | |
return () |
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
-- A newtype indexed by a sum type lifted with DataKinds. | |
-- Inspired by https://www.reddit.com/r/haskell/comments/elh8hl/newtype_or_tagged_type/ | |
-- See also | |
-- http://oleg.fi/gists/posts/2019-03-21-flag.html | |
-- https://hackage.haskell.org/package/safe-money | |
{-# LANGUAGE DataKinds #-} | |
{-# LANGUAGE ScopedTypeVariables #-} | |
{-# LANGUAGE TypeApplications #-} | |
{-# LANGUAGE AllowAmbiguousTypes #-} | |
module Main (main) where | |
-- To be used as a kind | |
data Scale = Celsius | Fahrenheit | |
-- | |
-- Annoying boilerplate, I want dependent Haskell to be able to demote | |
-- without boilerplate or TH | |
class DemoteScale (s :: Scale) where | |
demoteScale :: Scale | |
instance DemoteScale Celsius where | |
demoteScale = Celsius | |
instance DemoteScale Fahrenheit where | |
demoteScale = Fahrenheit | |
-- | |
-- constructor likely not exported | |
newtype Temperature (s :: Scale) = Temperature Int deriving (Show,Eq,Ord) | |
makeTemperature :: forall (s :: Scale) . Int -> Temperature s | |
makeTemperature = Temperature | |
getTemperatureLevel :: Temperature s -> Int | |
getTemperatureLevel (Temperature i) = i | |
-- | |
prettyPrintTemperature :: forall s. DemoteScale s => Temperature s -> String | |
prettyPrintTemperature t = | |
let unit = case demoteScale @s of | |
Celsius -> "°C" | |
Fahrenheit -> "°F" | |
in show (getTemperatureLevel t) ++ " " ++ unit | |
main :: IO () | |
main = do | |
putStrLn $ prettyPrintTemperature $ makeTemperature @Celsius 3 | |
putStrLn $ prettyPrintTemperature $ makeTemperature @Fahrenheit 3 |
@rampion It does seem like the same idea! I like the idea in Cascade of using a record type for storing the intermediate results.
I eventually went a bit overboard and in Pipelines3.hs I used a type-level map to have named stages.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Haven't finished reading your code yet, but your description reminds me of my cascades toy project from a couple years ago: