1

I just started exploring the possibilities of data types à la carte in combination with indexed types. My current experiment is a bit too large to include here, but can be found here. My example is mixing together an expression from different ingredients (arithmetic, functions, ...). The goal is to enforce only well-typed expressions. That is why an index is added to the expressions (the Sort type).

I can build expressions like:

-- define expressions over variables and arithmetic (+, *, numeric constants)
type Lia = IFix (VarF :+: ArithmeticF)

-- expression of integer type/sort
t :: Lia IntegralSort
t = var "c" .+. cnst 1

This is all good as long as I construct only fixed (static) expressions.

Is there a way to read an expression from string/other representation (that obviously has to encode the sort) and produce a dynamic value that gets represented by these functors?

For example, I would like to read ((c : Int) + (1 : Int)) and represent it somehow with VarF and ArithmeticF. Here I realize I cannot obtain a value of static type Lia IntegralSort. But suppose I have in addition:

data EqualityF a where
    Equals :: forall s. a s -> a s -> EqualityF a BoolSort

I could expect there being a function that can read String into Maybe (IFix (EqualityF :+: VarF :+: ...)). Such a function would attempt to build representations for the LHS and RHS and if the sorts matched it could produce a result of statically known type IFix (EqualityF :+: ...) BoolSort. The problem is that the representation of LHS (and RHS) has no fixed static sort. Is what I am trying to do impossible with this representation I chose?

jakubdaniel
  • 2,233
  • 1
  • 13
  • 20
  • 1
    "Here I realize I cannot obtain a value of static type Lia IntegralSort. " why not? It seems relatively straightforward to write a parser if the types are fixed. It becomes trickier if the output type depends on the input string, but it is still doable with existentials. `Data.Typeable` may help for your `Equals` example. – Li-yao Xia May 15 '17 at 00:42
  • This is exactly what I am saying... I am interested in types that depend on input values, fixed types are obviously ok. – jakubdaniel May 15 '17 at 06:28
  • Looking at your language it appears that you'll need to do some unification. (eg in the expression `Select arr ix`, if you know the type of `arr` you can infer the type of `ix`.) Implementing a unification algorithm and proving it correct in the type system is fun, but maybe a bit too big for a stack overflow answer, especially in clunky old Haskell. – Benjamin Hodgson May 15 '17 at 12:32
  • If fixed types are ok, that seems to contradict the fact that you "cannot obtain a value of static type `Lia IntegralSort`". – Li-yao Xia May 15 '17 at 12:32
  • 1
    The direct answer to your question is to [use existential quantification in the return type of your type checking function](http://stackoverflow.com/questions/33408943/parsing-and-the-use-of-gadts/33411022#33411022) – Benjamin Hodgson May 15 '17 at 12:38
  • @Li-yaoXia poor wording on my side. Point was that I dont hope the code to guarrantee a **fixed type** simply because the input string **value** can be anything, i.e. by calling`parse :: String -> FixedType` I impose the requirement on the return type instead of having that derived based on the actual value. – jakubdaniel May 15 '17 at 12:48

1 Answers1

1
(.=.) :: EqualityF :<: f => IFix f s -> IFix f s -> IFix f BoolSort
(.=.) a b = inject (Equals a b)

You can use a GADT to hide the sort, allowing you to return values of sorts depending on the input. Pattern matching then allows you to recover the sort.

data Expr (f :: (Sort -> *) -> (Sort -> *)) where
  BoolExpr :: IFix f BoolSort -> Expr f
  IntExpr  :: IFix f IntegralSort -> Expr f

Here is a simplistic parser of postfix expressions involving + and =.

parse :: (EqualityF :<: f, ArithmeticF :<: f) => String -> [Expr f] -> Maybe (Expr f)

parse (c : s) stack | isDigit c =
  parse s (IntExpr (cnst (digitToInt c)) : stack)

parse ('+' : s) (IntExpr e1 : IntExpr e2 : stack) =
  parse s (IntExpr (e1 .+. e2) : stack)

parse ('=' : s) (IntExpr e1 : IntExpr e2 : stack) =
  parse s (BoolExpr (e1 .=. e2) : stack)

parse ('=' : s) (BoolExpr e1 : BoolExpr e2 : stack) =
  parse s (BoolExpr (e1 .=. e2) : stack)

parse [] [e] = Just e
parse _ _ = Nothing

You might not like the duplicate cases for =. A more general framework is Typeable, allowing you to just test for the type equalities you need.

data SomeExpr (f :: (Sort -> *) -> Sort -> *) where
  SomeExpr :: Typeable s => IFix f s -> SomeExpr f


parseSome :: forall f. (EqualityF :<: f, ArithmeticF :<: f) => String -> [SomeExpr f] -> Maybe (Expr f)

parseSome (c : s) stack | isDigit c =
  parseSome s (SomeExpr (cnst (digitToInt c)) : stack)

parseSome ('+' : s) (SomeExpr e1 : SomeExpr e2 : stack) = do
  e1 <- gcast e1
  e2 <- gcast e2
  parseSome s (SomeExpr (e1 .+. e2) : stack)

parseSome ('=' : s) (SomeExpr (e1 :: IFix f s1) : SomeExpr (e2 :: IFix f s2) : stack) = do
  Refl <- eqT :: Maybe (s1 :~: s2) 
  parseSome s (SomeExpr (e1 .=. e2) : stack)

parseSome [] [e] = Just e
parseSome _ _ = Nothing

Edit

To parse sorts, you want to track them at the type level. Again, use an existential type.

data SomeSort where
  SomeSort :: Typeable (s :: Sort) => proxy s -> SomeSort

You can construct the sort of arrays this way:

-- \i e -> array i e
arraySort :: SomeSort -> SomeSort -> SomeSort
arraySort (SomeSort (Proxy :: Proxy i)) (SomeSort (Proxy :: Proxy e)) =
  SomeSort (Proxy :: Proxy (ArraySort i e))

A potential problem with Typeable here is that it only allows you to test equality of types, when you may want only to check the head constructor: you can't ask "is this type an ArraySort?", but only "is this type equal to ArraySort IntSort BoolSort?" or some other full type. In that case you need a GADT that reflects the structure of a sort.

-- "Singleton type"
data SSort (s :: Sort) where
  SIntSort :: SSort IntSort
  SBoolSort :: SSort BoolSort
  SArraySort :: SSort i -> SSort e -> SSort (ArraySort i e)

data SomeSort where
  SomeSort :: SSort s -> SomeSort

array :: SomeSort -> SomeSort -> SomeSort
array (SomeSort i) (SomeSort e) = SomeSort (SArraySort i e)

The singleton package provides various facilities for defining and working with these singleton types, though it may be overkill for your use case.

Li-yao Xia
  • 31,896
  • 2
  • 33
  • 56
  • I was probably looking for the `Typeable` approach, hardwiring the type beats the extensibility of data types a la carte imho, thanks for the pointers. – jakubdaniel May 15 '17 at 12:56
  • The more I examine this answer the more I like it. However, I could still use some explanations. Assuming the `Typeable` approach, how would one parse e.g. `(a : (array int bool))` for example? I am trying to use `attoparsec` and can easily parse that string into `name :: String` and `sort :: Sort`, I have a hard time figuring how to build a `IFix f s` to be wrapped into `SomeExpr` that has the proper sort. As demonstrated, doing the same for `IntegralSort` and `BooleanSort` is straightforward. – jakubdaniel May 15 '17 at 22:01
  • You can track the sorts you parse at the type level with another GADT. `data SomeSort where SomeSort :: forall (s :: Sort). Typeable s => proxy s -> SomeSort`. So when you parse a sort, return a value of type `SomeSort`. – Li-yao Xia May 15 '17 at 23:02
  • Could you please be more specific? I have `data SortProxy (s :: Sort)`, `data SomeSort`, but when my grammar matches the index sort and the element sort, I have two instances of `SomeSort` but I seem to run into errors when trying to merge the proxies with `SortProxy i -> SortProxy e -> SortProxy (ArraySort i e)` since `SomeSort` quantifies over `s`. Could you maybe put together an example of code that given two `SomeSort` produces `SomeSort` that holds `ArraySort` of the two given sorts? I would really appreciate it, typeable seems to be too cryptic for me (the doc does not explain much). – jakubdaniel May 16 '17 at 01:07
  • Thanks. Believe it or not, I tried almost exactly the same thing quite early on. What I didn't know was possible, however, was that you can give type signatures in pattern matches and further did not try using it with `ScopedTypeVariables`. – jakubdaniel May 16 '17 at 07:35
  • @jakubdaniel, my personal experience suggests: 1. If I'm using any other type system extension, I'm almost certain to want `ScopedTypeVariables` to express what I need. 2. If I'm not using any other type system extension, I'm almost certain to want `ScopedTypeVariables` so I can write more type signatures to help me fix type errors when I goof up. 3. If I don't enable `ScopedTypeVariables`, I'm likely to end up getting confused when I expect an instance head type variable to be in scope where it's not. So most of the time, I just enable `ScopedTypeVariables` before I start writing anything. – dfeuer May 18 '17 at 17:52