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.
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