Conkin
Tools for functors from Hask^k to Hask
Install / Use
/learn @rampion/ConkinREADME
One thing I haven't often seen people talk about doing in Haskell is working with data in column-major order, or as a struct of arrays. If we take a look though, there's some interesting possibilities and theory underlying this relatively simple concept.
The conkin package is the result of my explorations along this line of thinking.
An example of use
Suppose we have a list of items we wish to manipulate in column-major order:
items :: [Item]
items = [ chocolateBar, toiletPaper, ibuprofen ]
chocolateBar, toiletPaper, ibuprofen :: Item
chocolateBar = Item 0xDE1EC7AB1E "chocolate bar" 1.50
toiletPaper = Item 0xDEFEC8 "toilet paper" 9.99
ibuprofen = Item 0x43A1A11 "ibuprofen" 5.25
Using the Functor instance for lists, we can easily extract each field into its own list:
extractFields0 :: [Item] -> ([UPC], [String], [Double])
extractFields0 items = ( upc <$> items, name <$> items, price <$> items )
{-$-----------------------------------------------------------------------------
>>> extractFields0 items
( [ 0xDE1EC7AB1E , 0xDEFEC8 , 0x43A1A11 ]
, [ "chocolate bar" , "toilet paper" , "ibuprofen" ]
, [ 1.5 , 9.99 , 5.25 ]
)
-}
We've lost bit of semantic meaning, however, as we've switched from our own custom data type to a generic tuple. We can regain this meaning if we define a type specifically for a collection of items, parameterized by the collection type:
extractFields1 :: [Item] -> ItemF []
extractFields1 items = ItemF (upc <$> items) (name <$> items) (price <$> items)
{-$-----------------------------------------------------------------------------
>>> extractFields1 items
ItemF
{ _upc = [ 0xDE1EC7AB1E , 0xDEFEC8 , 0x43A1A11 ]
, _name = [ "chocolate bar" , "toilet paper" , "ibuprofen" ]
, _price = [ 1.5 , 9.99 , 5.25 ]
}
-}
data ItemF f = ItemF
{ _upc :: f UPC
, _name :: f String
, _price :: f Dollars
}
deriving instance (Show (f String), Show (f Dollars), Show (f UPC)) => Show (ItemF f)
deriving instance (Eq (f String), Eq (f Dollars), Eq (f UPC)) => Eq (ItemF f)
With a little help from PatternSynonyms we can derive the Item type from ItemF, making sure the two definitions don't slip out of step:
{-$-----------------------------------------------------------------------------
>>> items
[ ItemF
{ _upc = Identity 0xDE1EC7AB1E
, _name = Identity "chocolate bar"
, _price = Identity 1.5
}
, ItemF
{ _upc = Identity 0xDEFEC8
, _name = Identity "toilet paper"
, _price = Identity 9.99
}
, ItemF
{ _upc = Identity 0x43A1A11
, _name = Identity "ibuprofen"
, _price = Identity 5.25
}
]
-}
-- import Data.Functor.Identity (Identity(..))
-- ...
type Item = ItemF Identity
-- {-# LANGUAGE PatternSynonyms #-}
-- ...
pattern Item :: UPC -> String -> Dollars -> Item
pattern Item upc name price = ItemF (Identity upc) (Identity name) (Identity price)
upc :: Item -> UPC
upc = runIdentity . _upc
name :: Item -> String
name = runIdentity . _name
price :: Item -> Dollars
price = runIdentity . _price
So what else can we do with ItemF? We can't make it a Functor, it's got the wrong kind.
{-$-----------------------------------------------------------------------------
>>> instance Functor ItemF where fmap = undefined
<BLANKLINE>
...
• Expected kind ‘* -> *’, but ‘ItemF’ has kind ‘(* -> *) -> *’
• In the first argument of ‘Functor’, namely ‘ItemF’
In the instance declaration for ‘Functor ItemF’
-}
But it's still got this parameter that it's covariant and homogenous in - all the fields must use the same container of kind * -> *, and changing what container we're using should be easy.
So let's define a different Functor class for types of kind (k -> *) -> *.
{-$-----------------------------------------------------------------------------
>>> :i Conkin.Functor
class Conkin.Functor (f :: (k -> *) -> *) where
Conkin.fmap :: forall (a :: k -> *) (b :: k -> *).
(forall (x :: k). a x -> b x) -> f a -> f b
...
-}
-- import qualified Conkin
-- ...
instance Conkin.Functor ItemF where
fmap f (ItemF {..}) = ItemF
{ _upc = f _upc
, _name = f _name
, _price = f _price
}
Now we can use Conkin.fmap to convert an individual Item into a ItemF []
{-$-----------------------------------------------------------------------------
>>> :t Conkin.fmap (\(Identity x) -> [x])
Conkin.fmap (\(Identity x) -> [x])
:: Conkin.Functor f => f Identity -> f []
>>> Conkin.fmap (\(Identity x) -> [x]) chocolateBar
ItemF
{ _upc = [ 0xDE1EC7AB1E ]
, _name = [ "chocolate bar" ]
, _price = [ 1.5 ]
}
-}
We could stitch together multiple of these ItemF [] into one if ItemF [] had a Monoid instance:
extractFields2 :: [Item] -> ItemF []
extractFields2 = foldMap $ Conkin.fmap $ pure . runIdentity
{-$-----------------------------------------------------------------------------
>>> extractFields2 items
ItemF
{ _upc = [ 0xDE1EC7AB1E , 0xDEFEC8 , 0x43A1A11 ]
, _name = [ "chocolate bar" , "toilet paper" , "ibuprofen" ]
, _price = [ 1.5 , 9.99 , 5.25 ]
}
-}
-- import Control.Applicative (Alternative(..))
-- ...
instance Alternative a => Monoid (ItemF a) where
mempty = ItemF empty empty empty
left `mappend` right = ItemF
{ _upc = _upc left <|> _upc right
, _name = _name left <|> _name right
, _price = _price left <|> _price right
}
Of course we could do this before with extractFields1, but there's nothing specific to ItemF in the definition of extractFields2. The same definition would work for any Conkin.Functor that formed a Monoid:
{-$-----------------------------------------------------------------------------
>>> :t foldMap $ Conkin.fmap $ pure . runIdentity
foldMap $ Conkin.fmap $ pure . runIdentity
:: (Applicative b, Conkin.Functor f, Monoid (f b), Foldable t) =>
t (f Identity) -> f b
-}
Another useful monoid is ItemF Maybe. This could let us combine multiple partially specified items into one:
{-$-----------------------------------------------------------------------------
>>> mempty { _price = Just 2.99 }
ItemF { _upc = Nothing , _name = Nothing , _price = Just 2.99 }
>>> mempty { _price = Just 2.99 } <> mempty { _upc = Just 0x0 }
ItemF { _upc = Just 0x0 , _name = Nothing , _price = Just 2.99 }
-}
(Side note - I love being able to partially specify ItemF Maybe using mempty with record notation. All the succinctness of ItemF { _price = Just 2.99 }, but none of the missing fields.)
We can use <> (aka mappend) to transform a partially specified item into a fully specified one:
withDefaults0 :: ItemF Maybe -> Item
withDefaults0 partial = Conkin.fmap (Identity . fromJust) $ partial <> ItemF
{ _upc = Just 0x0
, _name = Just "unknown"
, _price = Just 0
}
{-$-----------------------------------------------------------------------------
>>> withDefaults0 mempty
ItemF
{ _upc = Identity 0x0
, _name = Identity "unknown"
, _price = Identity 0.0
}
>>> withDefaults0 mempty { _price = Just 2.99, _name = Just "flyswatter" }
ItemF
{ _upc = Identity 0x0
, _name = Identity "flyswatter"
, _price = Identity 2.99
}
-}
However, I'm not a big fan of this solution. We've abandoned some safety by using the partial fromJust. If a future developer alters a default to be Nothing, the compiler won't complain, we'll just get a runtime error.
What I'd rather be using is the safer fromMaybe, but since that's a two-argument function, I can't just use it via fmap. I need ItemF to be an Applicative.
We'll need a slightly different Applicative class than Prelude's, as ItemF again has the wrong kind:
{-$-----------------------------------------------------------------------------
>>> :i Conkin.Applicative
class Conkin.Functor f =>
Conkin.Applicative (f :: (k -> *) -
