Question

I'm writing an interpreter for a dynamically typed language in Haskell.

Like most dynamically typed language interpreters, my program is also needs to check types in runtime. One of the most used code in my program is this:

interpreterFunction :: Value -> SomeMonadicContext ReturnType
interpreterFunction p = do
    VStr s <- ensureType p TString
    ..
    some code that uses s

Here I'm ensuring that p has type TString, and after that I'm destructuring it with VStr s <- .... This never fails because VStr is the only value that has type TString.

My data structures are basically this:

data Value = VStr String | VInt Int | VBool Bool
data Type  = TStr | TInt | TBool

So I'm seperating my values depending on their types. ie. I have only one value constructor that has TStr as type.

Now I'm wondering if there's a way to simplify my ensureType function and destructuring code. For instance, is such a thing possible:

interpreterFunction p = do
    s <- ensureType p
    ..
    same code that uses s

Here from the code after s <- it can be deduced that s has type String, and it is statically known that only Value constructor that has a String part is VStr, so ensureType returns String after dynamically checking if p is a VStr.

I actually have no idea if this makes sense, or possible. I'm just looking for ways to improve my design with the help of Haskell's advanced type system features.

Any helps will be appreciated.

Was it helpful?

Solution

Yes, you can in fact do that with the help of type classes. Whether it's sensible or not is debatable (for your simple Value type, pattern matching is probably a better solution), but it's still interesting :)

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Main where

data Value = VStr String | VInt Int | VBool Bool

class FromValue a where
    fromValue :: Value -> Maybe a
instance FromValue String where
    fromValue (VStr s) = Just s
    fromValue _ = Nothing

ensureType :: (Monad m, FromValue a) => Value -> m a
ensureType = maybe (fail "type error!") return . fromValue

interpreterFunction :: Value -> IO ()
interpreterFunction val = 
    do s <- ensureType val
       putStrLn s

main =
    do interpreterFunction (VStr "asd")
       interpreterFunction (VInt 1)

Prints:

asd
*** Exception: user error (type error!)

You can also make use of the ScopedTypeVariables extension to force a specific type when it cannot be inferred:

{-# LANGUAGE ScopedTypeVariables #-}
interpreterFunction2 :: Value -> IO ()
interpreterFunction2 val = 
    do (s :: String) <- ensureType val
       print s

By the way, your initial approach seems a bit "unsound" to me:

VStr s <- ensureType p TString

Although you might be sure (by introspection) that ensureType x TString never returns anything but a string, this is not enforced by the type system and the pattern match is non-exhaustive. That's not a big problem here, but you can easily erradicate the possibility of a runtime failure in that function by using a special "string extraction" function instead:

ensureString :: (Monad m) => Value -> m String
{- ... -}
s <- ensureString val

OTHER TIPS

1. Clean, clear easy way:

I think the tagged union type you define

data Value = VStr String | VInt Int | VBool Bool

has all the runtime type checking you need built in as plain old pattern matching, and wrapping it in some advanced type system feature is missing a clean and clear solution:

interpreterFunction :: Value -> SomeMonadicContext ReturnType
interpreterFunction (Vstr s) = do
    some code that uses s
interpreterFunction _ = do
    some error handling code

You liked:

interpreterFunction p = do
    s <- ensureType p
    ..
    same code that uses s

saying

"Here from the code after s <- it can be deduced that s has type String, and it is statically known that only Value constructor that has a String part is VStr, so ensureType returns String after dynamically checking if p is a VStr.

My version also dynamically checks whether p is a VStr. It is statically known that the only Value constructor that has a String part is VStr, but that's actually quite hard to exploit.

2. Less clean typeclass based way

We'll need to make a String instance, so we need

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}

class FromValue a where 
   ensureType :: Value -> SomeMonadicContext a

Example instances:

instance FromValue String where
   ensureType (VStr s) = return s
   ensureType  _       = fail $ unlines 
                          ["Yikes, I'd rather have thrown a Nothing than a fail,"
                          ,"but then I'd have to have detagged the Just later"
                          ,"and then I may as well have used solution 1 anyway!"]

instance FromValue Int where
   ensureType (VInt i) = return i
   ensureType  _       = fail "Oh no, do I really have to use catch?"

Which would give

interpreterFunction :: Value -> IO String
interpreterFunction p = do
    s <- ensureType p
    return $ s ++ ", yay!" -- example String-specific code.

This has the disadvantage that it goes for failure instead of choosing an appropriate response to the user. You can use catch to recover, but again, you're doing an awful lot of work just to replicate the customisable case-handling features that the pattern matching solution gives.

2b. Maybe?

Using a Maybe data type in ensureType would avoid the annoying failure/catch, but then you would have to do Just s <- ensureType p or use the maybe function, all at least as much work as the plain pattern match.

3. Something clever and new but inappropriate

Alternatively, if your main aim is to use cool and newer type system features, you could use a GADT to replicate existential types, or Dynamic to do it in a more structured, dynamic-typing kind of way, but don't; neither of these has the clarity, simplicity and elegance of straightforward pattern matching.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top