Search code examples
haskellinterpreterdynamic-typing

Haskell - help simplifying a function with advanced type features


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.


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