Picnic: put containers into a backpack
by Dmitrii Kovanikov
HASKELL.SG: 6 Sep 2018
About me
What I do
Haskell Adept at Holmusk (present)
Contacts
Haskell Software Developer at Serokell (2016-2018)
Haskell Lecturer at the ITMO University (2015-2018)
Cofounder, Mentor, Developer at Kowainik (free time)
@chshersh
What is Backpack?
Backpack — mixin package system.
Mixin libraries can have signatures which permit implementations of values and types to be deferred, while allowing a library with missing implementations to still be type-checked.
New fields in .cabal files: signatures, reexported-modules, mixins
New file extension: .hsig
Supported currently only by cabal-install (stack issue)
New compiler errors!
Interface for containers
Why we need it?
1. Polymorphic functions.
groupBy :: (Foldable f, Ord k) => (a -> k) -> f a -> Map k (NonEmpty a)
ghci> groupBy even [1..10]
fromList [ (False, 9 :| [7,5,3,1])
, (True, 10 :| [8,6,4,2])
]
2. Benchmarks.
3. Property-based tests for laws (and unit tests as well)
* lookup k (insert k v m) ≡ Just v
* insert k b . insert k a ≡ insert k b
* member k (delete k m) ≡ False
Challenges
1. Data types are different across libraries (like Map and HashMap).
2. Every library has its own constraints for the keys (Map requires Ord constraint and HashMap requires Eq and Hashable constraints).
3. Types might have different kinds (consider Map and IntMap).
4. Some maps don’t have efficient modification operations since they are based on arrays (Map from primitive-containers package).
5. Different libraries implement the same functions with different constraints.
Typeclass-based solution (1 / 2)
class ( Monoid set
, MonoFoldable set
, Eq (ContainerKey set)
, GrowingAppend set
)
=> SetContainer set where
type ContainerKey set
member :: ContainerKey set -> set -> Bool
notMember :: ContainerKey set -> set -> Bool
union :: set -> set -> set
difference :: set -> set -> set
intersection :: set -> set -> set
keys :: set -> [ContainerKey set]
Typeclass-based solution (2 / 2)
class StaticMap t where
type Key t :: Type
type Val t :: Type
size :: t -> Int
lookup :: Key t -> t -> Maybe (Val t)
member :: Key t -> t -> Bool
class StaticMap t => DynamicMap t where
insert :: Key t -> Val t -> t -> t
insertWith :: (Val t -> Val t -> Val t) -> Key t -> Val t -> t -> t
delete :: Key t -> t -> t
alter :: (Maybe (Val t) -> Maybe (Val t)) -> Key t -> t -> t
Problems with typeclass-based solutions?
Backpack solution (1 / 4)
signature Map (Map, Key, empty, alter) where
data Map k v
class Key k
instance (Show k, Show v) => Show (Map k v)
empty :: Map k v
alter :: Key k => (Maybe v -> Maybe v) -> k -> Map k v -> Map k v
Map.hsig
cabal-version: 2.0
name: containers-sig
library
signatures: Map
build-depends: base
containers-sig.cabal
Backpack solution (2 / 4)
module Map.Contrib.Group (groupBy) where
import Map (Key, Map)
groupBy :: (Foldable f, Key k) => (a -> k) -> f a -> Map k (NonEmpty a)
groupBy = < ... some implementation ... >
Map/Contrib/Group.hs
cabal-version: 2.0
name: containers-contrib
library
exposed-modules: Map.Contrib.Group
build-depends: base, containers-sig
containers-contrib.cabal
Backpack solution (3 / 4)
{-# LANGUAGE ConstraintKinds #-}
module Map.Ord (Map, Key, empty, alter) where
import qualified Data.Map.Strict as M
type Map = M.Map
type Key = Ord
empty :: Map k v
empty = M.empty
alter :: Key k => (Maybe v -> Maybe v) -> k -> Map k v -> Map k v
alter = M.alter
Map/Ord.hs
{-# LANGUAGE ConstraintKinds #-}
module Map.Ord (Map, Key, module Map) where
import Data.Map.Strict as Map
type Key = Ord
cabal-version: 2.0
name: containers-ordered-strict
library
exposed-modules: Map.Ord
reexported-modules: Map.Ord as Map
build-depends: base, containers
containers-ordered-strict.cabal
Backpack solution (4 / 4)
module Main where
import Map.Contrib.Group (groupBy)
main :: IO ()
main = do
putStrLn "### Map ###"
print $ groupBy (`mod` 2) ([1..10] :: [Int])
Main.hs
cabal-version: 2.0
name: containers-example
executable map-exe
main-is: Main.hs
build-depends: base
, containers-ordered-strict
, containers-contrib
containers-example.cabal
Backpack HashMap
{-# LANGUAGE ConstraintKinds #-}
module Map.Hash where
import Data.Hashable (Hashable)
import Data.HashMap.Strict as HM
type Map = HM.HashMap
type Key a = (Eq a, Hashable a)
{-# LANGUAGE ConstraintKinds #-}
module Map.Ord where
import Data.Map.Strict as M
type Map = M.Map
type Key = Ord
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE UndecidableInstances #-}
module Map.Hash where
import Data.Hashable (Hashable)
import Data.HashMap.Strict as HM
type Map = HM.HashMap
class (Eq k, Hashable k) => Key k
instance (Eq k, Hashable k) => Key k
• Type constructor ‘Key’ has conflicting definitions in the module
and its hsig file
Main module: type Key a =
(ghc-prim-0.5.2.0:GHC.Classes.Eq a,
hashable-1.2.7.0:Data.Hashable.Class.Hashable a)
:: Constraint
Hsig file: class Key k
Illegal parameterized type synonym in implementation of abstract data.
(Try eta reducing your type synonym so that it is nullary.)
• while checking that containers-unordered-strict-0.0.0:Map.Hash
implements signature Map in
containers-contrib-0.0.0[Map=containers-unordered-strict-0.0.0:Map.Hash]
Using mixins
cabal-version: 2.0
name: containers-example
executable map-exe
main-is: Main.hs
build-depends: base
, containers-ordered-strict
, containers-unordered-strict
, containers-contrib
mixins: containers-contrib (Map.Contrib.Group as Map.Contrib.Group.Ord)
requires (Map as Map.Ord)
, containers-contrib (Map.Contrib.Group as Map.Contrib.Group.Hash)
requires (Map as Map.Hash)
containers-example.cabal
import qualified Map.Contrib.Group.Hash as HM (groupBy)
import qualified Map.Contrib.Group.Ord as M (groupBy)
main :: IO ()
main = do
putStrLn "### Map ###"
print $ M.groupBy (`mod` 2) ([1..10] :: [Int])
putStrLn "### HashMap ###"
print $ HM.groupBy (`mod` 2) ([1..10] :: [Int])
Main.hs
Backpack IntMap
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
module Map.Int
( Map
, Key
, empty
, alter
) where
import qualified Data.IntMap.Strict as M
newtype Map k v = IM { unIM :: M.IntMap v }
deriving newtype (Show)
type Key = (~) Int
empty :: Map k v
empty = IM M.empty
alter :: Key k => (Maybe v -> Maybe v) -> k -> Map k v -> Map k v
alter f k = IM . M.alter f k . unIM
Map/Int.hs
Problems with IntMap
newtype Map k v = IM { unIM :: M.IntMap v }
deriving newtype (Show)
type Key = (~) Int
-- Doesn't compile without 'Key' constraint!
toList :: Map k v -> [(k, v)]
Map/Int.hs
Map.hsig
signature Map where
...
toList :: Key k => Map k v -> [(k, v)]
Read-only containers
signature ROMap (Map, Key, lookup, ...)
What doesn't work
Maps and Sets from primitive-containers package don't have modification operations. This means we need to split our signatures across two packages.
What works
signature Map (Map, Key, lookup, ...)
data Map k v
class Key k
lookup :: Key k
=> k
-> Map k v
-> Maybe v
signature Map (insert, delete, ...)
import ROMap (Map, Key)
containers-sig-readonly:Map.hsig
containers-sig:Map.hsig
signature Map (Map, Key, delete, ...)
data Map k v
class Key k
delete :: Key k
=> k
-> Map k v
-> Map k v
Backpack in the wild
* haskell-backpack/backpack-str: String signatures
* unpacked-containers: ordered containers with unpacked keys
* ezyang/backpack-regex-example: Learning examples
* ezyang/reflex-backpack: Reflex specialized to Spider
* kowainik/containers-backpack: Backpack interface for containers
* hasktorch/hasktorch: Haskell Backpack bindings for PyTorch
How to spend free time
Questions?
Picnic: put containers into a Backpack
By Dmitrii Kovanikov
Picnic: put containers into a Backpack
Slides for talk about Backpack interface for containers.
- 1,875