Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type checked decomposable graph in Haskell

Let's say I'm creating a data pipeline which will process text files. I have the following types and functions:

data A = A deriving (Show, Typeable)
data B = B deriving (Show, Typeable)
data C = C deriving (Show, Typeable)
data D = D deriving (Show, Typeable)

step1 :: A -> B
step2 :: B -> C
step3 :: C -> D

For each of the functions step{1..3} below I would like to be able to do produce a new file from an existing file, doing something like:

interact (lines . map (show . step . read) . unlines)

I then want to be able to arrange them into a graph (so functions can have multiple inputs) to achieve the following:

  1. I can traverse the data structure to tell which functions provide inputs to which others

  2. The data structure will type check at compile time so that any invalid arrangement throws a type error.

I know how to do 1 without 2 (give them a common typeclass), and I know how to do 2 without 1 (just use (.)), but I'm not sure how to do both at once. Any ideas? TIA.

Update

AndrewC's answer is pretty much spot on and gets me most of the way there, since I can then build the graph of metadata (names) using a Map and type check it separately at the same time, however I also need some flexible polymorphism. The example below solves this but with 2 serious caveats:

  1. I'm not forced to be exhaustive in pattern matching in step4.
  2. It's tedious; there could be dozens of these "BorC" style polymorphic types, which do nothing except enable polymorphism, and not even very safely (see 1). I tried to pattern match on members of a type class (Step4Input with instances for B and C) but that didn't work (it said Couldn't match type B with C).
data BorC = ItsB B | ItsC C

step4 :: BorC -> D
step4 x = case x of { ItsB b -> D; ItsC c -> D }

-- step1    step2
--    \       /
--   ItsB   ItsC
--     \   /
--     step4
like image 677
Scott Avatar asked Aug 08 '26 18:08

Scott


1 Answers

I may be misunderstanding your problem, but why not simply wrap the functions up with a name?
It seems a very straightforward approach to the situation which doesn't require a lot of messing about.

module NamedFunctions where

import Control.Category        -- so I can generalise (.)
import Prelude hiding ((.),id) -- don't worry, available from Category

Store a list of function names you've composed along with the function itself:

data Fn name a b = Fn {names::[name],apply:: (a -> b)} 

When you show a function, just show what you composed to get there:

instance Show name => Show (Fn name a b) where
  show f = show $ names f

Define new versions of (.) and ($):

infixr 9 ...
(...) :: Fn name b c -> Fn name a b -> Fn name a c
f ... g = Fn (names f ++ names g) (apply f . apply g)

infixr 0 $$$
($$$) :: Fn name a b -> a -> b
f $$$ x = apply f x

and reclaim (.) to work on named functions

instance Category (Fn name) where
   (.) = (...)
   id = Fn [] id

You can leave functions you don't care about unnamed so they don't contribute to the names list.

name  n f = Fn [n] f
unnamed f = Fn []  f

You can use whatever data you like to name each function, for example:

timesThree = name "(*3)" (*3)
addFour = name "(+4)" (+4)

step1 :: Fn Int A B 
step1 = name 1 undefined
step2 :: Fn Int B C 
step2 = name 2 undefined
step3 :: Fn Int C D 
step3 = name 3 undefined


*NamedFunctions> timesThree . addFour
["(*3)","(+4)"]
*NamedFunctions> timesThree . addFour $$$ 5
27
*NamedFunctions> step3.step2.step1
[3,2,1]
*NamedFunctions> step3.step2.step1 $$$ A
*** Exception: Prelude.undefined
like image 120
AndrewC Avatar answered Aug 10 '26 17:08

AndrewC



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!