7

Can one define a Haskell type at runtime from a given template? Here's what I mean by this. Suppose I need an integer type that is restricted to some range (unknown precisely at compile time). I also want a feature that:

succ 0 = 1
succ 1 = 2
...
succ n = 0

n being unknown at compile time. I could do something like this:

data WrapInt = WrapInt {
        value       :: Int,
        boundary    :: Int
}

wrapInt :: Int -> Int -> WrapInt
wrapInt boundary value = WrapInt value boundary

Now what I would like to have is to preserve the wrapInt function as it is, but to avoid storing the boundary as a value inside WrapInt type. Instead I would like it to be stored somehow in type definition, which of course means that the type would have to be defined dynamically at runtime.

Is it possible to achieve this in Haskell?

Sventimir
  • 1,996
  • 3
  • 14
  • 25
  • 3
    You must store the bound *somewhere*. If it's not known compile time, then it must be present runtime, either as a field of a structure or passed in as an argument to a function. – András Kovács Apr 27 '15 at 18:26
  • If I'm understanding, you want to turn a runtime value into a type essentially? So that you would be able to something like `newtype WrapInt n = WrapInt Int; wrapInt :: Int -> Int -> WrapInt n`, where `n` represents the runtime value and then let the other functions act accordingly? – David Young Apr 27 '15 at 18:40
  • Yes, that's exactly what I want. – Sventimir Apr 27 '15 at 18:46
  • In haskell, types exist only at compile time. If you have the range available at compile time, you can use a type. If you have it available only at runtime, it needs a runtime representation. – Carl Apr 27 '15 at 18:48
  • What is your use case for this? Generally, types represent things that are available at compile time and a field in a record represents something that is only available at run time, so I'm wondering why the last bit of code you have in your question isn't what you're looking for. – David Young Apr 27 '15 at 18:55
  • 1
    You are looking for [finite sets](http://stackoverflow.com/questions/7209100/a-definition-for-finite-sets-in-agda) (see also [here](http://stackoverflow.com/questions/18726164/how-can-finite-numbers-work-dependent-types)), probably. I wrote some [explanations](http://stackoverflow.com/a/27070207/3237465) a while ago, which involved them. – effectfully Apr 27 '15 at 19:19
  • The use case is simply this: I need an Integer-like type where `succ n = 0` for some arbitrary value of `n`, which is unknown at compile time. It would serve as a counter, which I would like to be able to increment fearlessly without checking if I get out of range. I don't like storing the boundary inside the value, because it's really a property of a whole type. I understand, though, it can't be done in Haskell. I'll have a look at the finite sets thing. Thanks. – Sventimir Apr 27 '15 at 21:37
  • 1
    Consider that because the type would be created at runtime, there could be more than one of them (different values of n). So logically each wrapped int needs to contain some information identifying which type it's actually a member of, so that operations on it can use the correct implementation for the type. Isn't that `boundary` field basically just such type information? You could make another data type representing the "type", and have each `WrapInt` instead store a reference to its "type object"... But what data would `WrapIntType` contain? Exactly the `boundary` value! – Ben Apr 27 '15 at 23:46
  • Does `reflection` have anything to offer? This sounds a lot like what Oleg et al called the configuration problem. – dfeuer Apr 28 '15 at 00:59

2 Answers2

8

The reflection package lets you generate new "local" instances of a typeclass at runtime.

For example, suppose we have the following typeclass of values that can "wrap around":

{-# LANGUAGE Rank2Types, FlexibleContexts, UndecidableInstances #-}

import Data.Reflection
import Data.Proxy

class Wrappy w where
   succWrappy :: w -> w

We define this newtype that carries a phantom type parameter:

data WrapInt s = WrapInt { getValue :: Int } deriving Show

An make it an instance of Wrappy:

instance Reifies s Int => Wrappy (WrapInt s) where
    succWrappy w@(WrapInt i) = 
        let bound = reflect w 
        in
        if i == bound
            then WrapInt 0
            else WrapInt (succ i)

The interesting part is the Reifies s Int constraint. It means: "the phantom type s represents a value of type Int at the type level". Users never define an instance for Reifies, this is done by the internal machinery of the reflection package.

So, Reifies s Int => Wrappy (WrapInt s) means: "whenever s represent a value of type Int, we can make WrapInt s an instance of Wrappy".

The reflect function takes a proxy value that matches the phantom type and brings back an actual Int value, which is used when implementing the Wrappy instance.

To actually "assign" a value to the phantom type, we use reify:

-- Auxiliary function to convice the compiler that
-- the phantom type in WrapInt is the same as the one in the proxy
likeProxy :: Proxy s -> WrapInt s -> WrapInt s
likeProxy _ = id

main :: IO ()
main = print $ reify 5 $ \proxy -> 
    getValue $ succWrappy (likeProxy proxy (WrapInt 5))

Notice that the signature of reify forbids the phantom type from escaping the callback, that's why we must unwrap the result with getValue.

See more examples in this answer, on in the reflection GitHub repo.

Community
  • 1
  • 1
danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • `reify (a :: k)` is a lot of magic, wow! First of all, behind the scene's it is implemented using `unsafeCoerce` - and I don't see another way. The idea of the implementation is that to fulfill the claim we would have to know some type `s` such that `reflect (Proxy :: Proxy s) = a`. Of course, this is impossible. But it such an s would exist, we can find a runtime representation of `Reifies s k` - which is after all simply `proxy s -> k`. `const a` fits the bill and is unsafeCoerced into place. And since `s` is existentially quantified it doesn't matter, can not escape, compare `ST s`. – WorldSEnder Aug 01 '19 at 22:16
4

It's not impossible — just very ugly. We'll need natural numbers

data Nat = Z | S Nat

and bounded natural numbers

data Bounded (n :: Nat) where
    BZ :: Bounded n
    BS :: Bounded n -> Bounded (S n)

Then your function should be something like

succ :: Bounded n -> Bounded n
succ bn = fromMaybe BZ $ go bn where
    go :: Bounded n -> Maybe (Bounded n)
    go = ...

In go we need to

  1. map BZ to Nothing, if n is Z (i.e. if a Bounded achieved its maximum and has overflowed)
  2. map BZ to Just (BS BZ), if n is not Z (i.e. if a Bounded didn't achieve its maximum).
  3. call go recursively for the BS case.

The problem however is that there is no way to get n at the value level. Haskell is not that dependent. The usual hack is to use singletons. Writing it manually

data Natty (n :: Nat) where
    Zy :: Natty Z
    Sy :: Natty n -> Natty (S n)

class NATTY (n :: Nat) where
    natty :: Natty n

instance NATTY Z where
    natty = Zy

instance NATTY n => NATTY (S n) where
    natty = Sy natty

Now we can get a value-level representation of n in the Bounded n in go:

succ :: NATTY n => Bounded n -> Bounded n
succ bn = fromMaybe BZ $ go natty bn where
    go :: Natty n -> Bounded n -> Maybe (Bounded n)
    go  Zy      BZ     = Nothing
    go (Sy ny)  BZ     = Just (BS BZ)
    go (Sy ny) (BS bn) = BS <$> go ny bn

And the NATTY type class is used to infer this value automatically.

Some tests:

instance Eq (Bounded n) where
    BZ    == BZ    = True
    BS bn == BS bm = bn == bm
    _     == _     = False

zero :: Bounded (S (S Z))
zero = BZ

one :: Bounded (S (S Z))
one = BS BZ

two :: Bounded (S (S Z))
two = BS (BS BZ)

main = do
    print $ succ zero == zero -- False
    print $ succ zero == one  -- True
    print $ succ one  == two  -- True
    print $ succ two  == zero -- True

The code.


Using the singletons library we can define succ as

$(singletons [d| data Nat = Z | S Nat deriving (Eq, Show) |])

data Bounded n where
    BZ :: Bounded n
    BS :: Bounded n -> Bounded (S n)

succ :: SingI n => Bounded n -> Bounded n
succ bn = fromMaybe BZ $ go sing bn where
    go :: Sing n -> Bounded n -> Maybe (Bounded n)
    go  SZ      BZ     = Nothing
    go (SS ny)  BZ     = Just (BS BZ)
    go (SS ny) (BS bn) = BS <$> go ny bn

As to lifting runtime stuff to the type level, there are two approaches: CPS and existential types.

Community
  • 1
  • 1
effectfully
  • 12,325
  • 2
  • 17
  • 40
  • I can't help wondering if some kind signatures would make this look a bit less mysterious. – dfeuer Apr 28 '15 at 01:02
  • @dfeuer, I used the `DataKinds`extension, which allows to treat datatypes as kinds, so everything (`Bounded`, `Natty` and `NATTY`) receives a `Nat`. The [Hasochism](https://personal.cis.strath.ac.uk/conor.mcbride/pub/hasochism.pdf) paper describes dependently typed programming in Haskell properly. **pigworker** also gave great explanations [here](http://stackoverflow.com/questions/28713994/why-is-the-type-system-refusing-my-seemingly-valid-program). – effectfully Apr 28 '15 at 01:33
  • I'm just suggesting that if you add kind signatures to everything in the `Nat` kind, that might make the code easier to read. – dfeuer Apr 28 '15 at 04:52
  • Thank you very much for your broad and detailed answer. But now, that I think of it, what Ben wrote under my initial post is quite right. Storing the boundary at value level is not very nice to my taste, but is a whole lot simpler solution, so I'll employ it. But this stuff I should analyze anyway just to learn something new. Thanks again. – Sventimir Apr 28 '15 at 05:57
  • 1
    @dfeuer, OK, I added them. – effectfully Apr 28 '15 at 09:17
  • @Sventimir, you're welcome. I understand your choice: this solution might be overkill even in a properly typed language. In Haskell you will quickly run in a situation, where you need to use `TypeFamilies`, enable `UndecidableInstances` and then guess what GHC can and what not. – effectfully Apr 28 '15 at 09:21