natefaubion / purescript-variant Goto Github PK
View Code? Open in Web Editor NEWPolymorphic variants for PureScript
License: MIT License
Polymorphic variants for PureScript
License: MIT License
This is better than Maybe
:) E.g. no need in conversion to List
, MaybeT
and EitherT
, Aff
etc.
Right now, Functor
is the only constraint with special consideration for VariantF
. Whenever you build a VariantF
with inj
, it captures the Functor
dictionary and keeps it around with the value. Since this is the defining evidence for VariantF
, dispatching it is free since we always have the appropriate dictionary on hand. For anything else (Foldable
, Traversable
, etc) we have to collect all possible dictionaries at the call site, and perform a lookup to dispatch the appropriate dictionary. This is inefficient all around. It would be nice if we could let users inject arbitrary evidence whenever we build a VariantF
, and use that to dispatch dictionaries.
The following is an example of some machinery that accomplishes this, but I'd like to get some feedback.
module DictTest where
import Prelude
import Data.Eq (class Eq1, eq1)
import Data.Foldable (class Foldable, foldMap, foldl, foldr)
import Data.Traversable (class Traversable, sequence, traverse)
newtype FunctorDict f =
FunctorDict
(forall a b. (a -> b) -> f a -> f b)
data FoldableDict f =
FoldableDict
(forall a r. (a -> r -> r) -> r -> f a -> r)
(forall a r. (r -> a -> r) -> r -> f a -> r)
(forall a r. Monoid r => (a -> r) -> f a -> r)
data TraversableDict f =
TraversableDict
(forall a b m. Applicative m => (a -> m b) -> f a -> m (f b))
(forall a m. Applicative m => f (m a) -> m (f a))
newtype Eq1Dict f =
Eq1Dict
(forall a. Eq a => f a -> f a -> Boolean)
class Gather (f :: Type -> Type) e | e -> f where
gather :: e f
class Dict (f :: Type -> Type) d e | d e -> f where
dict :: e f -> d f
data Boxed (e :: (Type -> Type) -> Type) (f :: Type -> Type) a = Boxed (e f) (f a)
box :: forall e f a. Gather f e => f a -> Boxed e f a
box = Boxed gather
instance functorBoxed :: Dict f FunctorDict e => Functor (Boxed e f) where
map k (Boxed e a) = case dict e of FunctorDict map' -> Boxed e (map' k a)
instance foldableBoxed :: Dict f FoldableDict e => Foldable (Boxed e f) where
foldr a b (Boxed e c) = case dict e of FoldableDict foldr' _ _ -> foldr' a b c
foldl a b (Boxed e c) = case dict e of FoldableDict _ foldl' _ -> foldl' a b c
foldMap a (Boxed e b) = case dict e of FoldableDict _ _ foldMap' -> foldMap' a b
instance traversableBoxed ::
( Dict f TraversableDict e
, Dict f FoldableDict e
, Dict f FunctorDict e
) => Traversable (Boxed e f) where
traverse a (Boxed e b) = case dict e of TraversableDict traverse' _ -> Boxed e <$> traverse' a b
sequence (Boxed e a) = case dict e of TraversableDict _ sequence' -> Boxed e <$> sequence' a
instance eq1Boxed :: Dict f Eq1Dict e => Eq1 (Boxed e f) where
eq1 (Boxed e a) (Boxed _ b) = case dict e of Eq1Dict eq1' -> eq1' a b
---
newtype Evidence f = Evidence
{ functor :: FunctorDict f
, foldable :: FoldableDict f
, traversable :: TraversableDict f
, eq1 :: Eq1Dict f
}
instance gatherEvidence :: (Traversable f, Eq1 f) => Gather f Evidence where
gather = Evidence
{ functor: FunctorDict map
, foldable: FoldableDict foldr foldl foldMap
, traversable: TraversableDict traverse sequence
, eq1: Eq1Dict eq1
}
instance evidenceFunctorDict :: Dict f FunctorDict Evidence where
dict (Evidence { functor }) = functor
instance evidenceEq1Dict :: Dict f Eq1Dict Evidence where
dict (Evidence { eq1 }) = eq1
instance evidenceFoldableDict :: Dict f FoldableDict Evidence where
dict (Evidence { foldable }) = foldable
instance evidenceTraversableDict :: Dict f TraversableDict Evidence where
dict (Evidence { traversable }) = traversable
The Boxed
type just packages up some evidence with some functor. If we extend that with a tag, then we'd have VariantFRep
, and we could implement all the instance in terms of the user injected evidence. The smart constructor ensures that it's only every built using gather
, which means that we always get the same dictionaries, so they are interchangeable in the case of binary operations like eq1
.
VariantF
is an extensible version of Coproduct
. We should have a RecordF
for Product
.
Not sure about the names but I am interested in two definitions like this:
disassociate :: forall u v s
. Contractable s u
=> R.Union u v s
=> Variant s
-> Either (Variant u) (Variant v)
disassociate s =
case contract s of
Just u -> Left u
Nothing -> Right (unsafeCoerce s)
associate :: forall u v s
. R.Union u v s
=> R.Union v u s
=> Either (Variant u) (Variant v)
-> Variant s
associate (Left u) = expand u
associate (Right v) = expand v
Idealistically, the conversions would be with a variant of variants, but the inference for a big-union constraint I am anticipating to be a nightmare. This is more simplistic.
single :: forall s t e
. IsSymbol s
=> RowCons s t () e
=> RowToList e (Cons s t Nil)
=> Variant e
-> t
single = on (SProxy :: SProxy s) id case_
Just an idea. I found this useful. Thanks to @MonoidMusician for the RowToList
idea so s
is inferrable.
Something like the following:
unVariant :: forall r x.
(forall s t o. IsSymbol s => RowCons s t o r => SProxy s -> t -> x) ->
Variant r ->
x
Most (all?) of the current variant functions appear to require upfront knowledge of which key you want to deal with. The proposed function should let us deal with whichever key was actually used.
(cc @erisco)
Proxy
is re-exported by Variant
:
purescript-variant/src/Data/Variant.purs
Line 38 in 00629d1
However, as this is now in the Prelude it seems like this is an unnecessary re-export.
Either
has bimap
, would be nice to have something like that for variant which takes record of functions, something like this:
f :: forall r. Variant (foo :: String, baz :: Int | r) -> Variant (foo :: Array String, baz :: Number | r)
f = V.map
{ foo: \s -> [s, "foo"]
, baz: toNumber >>> _ + 9.0
}
Just like there are prop
lenses for records, there can be prisms for variants. Here is a way to implement it, no sure if it is possible to do without unsafeCoerce. (I couldn't get expand
to compile here.)
label :: forall l r1 r2 r a b. IsSymbol l => Cons l a r r1 => Cons l b r r2 => SProxy l -> Prism (Variant r1) (Variant r2) a b
label l = prism (inj l) (on l Right (expand' >>> Left))
where
expand' :: Variant r -> Variant r2
expand' = unsafeCoerce
This depends on having records lib in core.
Just driving by on pursuit, VariantF has its arguments both annotated as phantom. This seems incorrect?
new version of https://github.com/paf31/purescript-foreign-generic implements these classes for record (https://github.com/paf31/purescript-foreign-generic/blob/46f09996bd54efc146bc1725783789dbac7d6a5b/src/Foreign/Generic/Class.purs#L218-L219)
maybe useful example https://github.com/justinwoo/purescript-simple-json/blob/master/src/Simple/JSON.purs#L245
I think we can derive Bounded, Enum and BoundedEnum for Variant, an it would be pretty useful, as for normal sum types we already have Generic.Rep.{Bounded,Enum}
.
"purescript-record": "0.2.3"
is fixed in bower while other deps use ^
. Is there any particular reason?
Is there any interest in adding something that allows constructing a variant with a singleton record?
foo :: Data.Variant.Variant (bar :: Int, baz :: String, qux :: Boolean)
foo = Data.Variant.fromRecord { qux: true }
The motivation here is that using SProxy _
is a bit more cumbersome than seems necessary. Record syntax is a bit more lightweight to toss around.
It seems like fromRecord
could be implemented like:
module Data.Variant where
class FromRecord (list :: Prim.RowList.RowList) record variant where
fromRecord' :: forall proxy. proxy list -> Record record -> Variant variant
instance fromRecordSingleton ::
( Data.Symbol.IsSymbol label
, Prim.Row.Cons label value () record
, Prim.Row.Cons label value variant' variant
) =>
FromRecord (Prim.RowList.Cons label value Prim.RowList.Nil) record variant
where
fromRecord' _ record = inj label (Record.get label record)
where
label :: Data.Symbol.SProxy label
label = Data.Symbol.SProxy
fromRecord ::
forall list record variant.
Prim.RowList.RowToList record list =>
FromRecord list record variant =>
Record record ->
Variant variant
fromRecord = fromRecord' (Type.Data.RowList.RLProxy :: _ list)
Might want some fundeps, dunno. If we want to be helpful, adding some custom error messages for using the wrong size record should be possible as well:
else instance fromRecordMultiple ::
( Prim.TypeError.Fail ?someErrorMessageAboutMultipleFieldsInTheRecord
) =>
FromRecord (Prim.RowList.Cons label value list) record variant
where
fromRecord' proxy record = fromRecord' proxy record
else instance fromRecordEmpty ::
( Prim.TypeError.Fail ?someErrorMessageAboutTheEmptyRecord
) =>
FromRecord Prim.RowList.Nil record variant
where
fromRecord' proxy record = fromRecord' proxy record
Since it does a naive Union
, it can introduce duplicate labels, which will lead to runtime errors since our semantics match records. This needs some typelevel-prelude wizardry to remove duplicates.
Could you publish a new version with the pulp version format, i.e. v6.0.1? Thanks
I needed something like this at one point let me know if you like this and will open PR.
mapLabel :: forall trash a b sym rIn rOut rBase
. IsSymbol sym
=> RowCons sym a rBase rIn
=> RowCons sym b rBase rOut
=> SProxy sym
-> (a -> b)
-> Variant rIn
-> Variant rOut
mapLabel p f r =
-- NOTE: This coerce is safe as we know `rBase` is subset of `rOut`. `expand` could
-- be used instead, but than you need to provide `Union` instances when it's used.
on p (inj p <<< f) (unsafeCoerce :: Variant rBase -> Variant rOut) r
Right now, a Variant
only permits a single value to be associated with a tag:
newtype VariantRep a = VariantRep
{ type :: String
, value :: a
}
foreign import data Variant :: Row Type -> Type
data OneOrPair0
= One Int
| Pair Int Int
type OneOrPair1 = Variant
( one :: Int
, pair :: Tuple Int Int
)
If one wants to define a Variant
-based List
type, they pay for an extra level of boxing:
newtype List a = List (
Variant
( nil :: Unit
-- extra nesting occurs here due to usage of
-- `Tuple` to store 2 args
, cons :: Tuple a (List a)
)
)
_nil = Proxy :: Proxy "nil"
_cons = Proxy :: Proxy "cons"
nil = List $ V.inj _nil unit
cons head tail = List $ V.inj cons_ $ Tuple head tail
cons12 = cons 1 $ cons 2 nil
{-
which has the following runtime representation on JS:
{ type: "cons"
, value:
{ tag: "Tuple" -- extra nesting
, value0: 1
, value1:
{ type: "cons"
, value:
{ tag: "Tuple" -- extra nesting
, value0: 2
, value1:
{ type: "nil"
, value: undefined
}
}
}
}
}
-}
as opposed to just using List
directly:
cons12 = Cons 1 $ Cons 2 Nil
{-
{ type: "Cons"
, value0: 1
, value1:
{ tag: "Cons"
, value0: 2
, value1:
{ type: "Nil"
}
}
}
-}
AFAICT, Variant
could denest this extra boxing by using the following representation, which I call Variance
to distinguish it from the current Variant
. I show both Variant
and Variance
below to show the small change made:
newtype VariantRep a = VariantRep
{ type :: String
, value :: a
}
newtype VarianceRep r = VarianceRep
{ __ctorTag :: String
| r
}
foreign import data Variant :: Row Type -> Type
foreign import data Variance :: Row (Row Type) -> Type
data OneOrPair0
= One Int
| Pair Int Int
type OneOrPair1 = Variant
( one :: Int
, pair :: Tuple Int Int
)
type OneOrPair2 = Variance
( one :: (int :: Int)
, pair :: (fst :: Int, snd :: Int)
)
pairVariant :: OneOrPair1
pairVariant = V.inj _pair $ Tuple 1 2
{-
Runtime representation:
{ type: "pair"
, value:
{ tag: "Tuple"
, value0: 1
, value1: 2
}
}
-}
pairVariance :: OneOrPair2
pairVariance = Variance.inj _pair { fst: 1, snd: 2 }
{-
Runtime representation:
{ __ctorTag: "pair"
, fst: 1
, snd: 2
}
-}
Variance.inj
is implemented as:
inj
:: forall sym ctorRows ctorsTail ctorsAll
. Row.Lacks "__ctorTag" ctorRows
=> Row.Cons sym ctorRows ctorsTail ctorsAll
=> IsSymbol sym
=> Proxy sym
-> { | ctorRows }
-> Variance ctorsAll
inj p value = coerceV $ VarianceRep $ Record.insert __ctorTag (reflectSymbol p) value
where
coerceV :: VarianceRep ctorRows → Variance ctorsAll
coerceV = unsafeCoerce
Similarly, Variance.on
is implemented as below. Notably, the entire representation is passed to the function so as not to duplicate the record without the __ctorTag
field:
on
:: forall sym ctorRows remaining all b
. Row.Cons sym ctorRows remaining all
=> Reflectable sym String
=> Proxy sym
-> ({ __ctorTag :: String | ctorRows } -> b)
-> (Variance remaining -> b)
-> Variance all
-> b
on p f g r = case coerceR r of
VarianceRep rep | rep.__tag == reflectSymbol p -> f rep
_ -> g (coerceTail r)
where
coerceR :: Variance all -> VarianceRep ctorRows
coerceR = unsafeCoerce
coerceTail :: Variance all -> Variance remaining
coerceTail = unsafeCoerce
By using the above representation, it enables the following Variance
-based List
type:
type NIL :: forall k. Row (Row k) -> Row (Row k)
type NIL r = (nil :: () | r)
_nil = Proxy :: Proxy "nil"
type CONS :: forall k. k -> k -> Row (Row k) -> Row (Row k)
type CONS a tail r = (cons :: (head :: a, tail :: tail) | r)
_cons = Proxy :: Proxy "cons"
newtype List :: Type -> Type
newtype List a = List
(Variance
( nil :: ()
, cons :: (head :: a, tail :: List a)
)
)
Besides the smaller boxing, the Nil
case also doesn't specify an unused value
as it's runtime representation is just { __ctorTag: "nil" }
. If one wanted to remove even that boxing and just have the string "Nil"
for constructors that have no arguments, then FFI would need to be added that does a typeof x === "string"
check.
contractToAce
∷ ∀ rb rs rl
. R.RowToList (AceR ()) rl
⇒ VariantTags rl
⇒ Union (AceR ()) rs rb
⇒ Variant rb
→ Maybe (Variant (AceR ()))
contractToAce v = contract
contractToAce
:: forall r
. Contractable r (AceR ())
=> Variant r
=> Maybe (Variant (AceR ()))
contractToAce = contract
Remembering that contract
must have 3 constraints is a bit too complex, why not aggregate them?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.