If I understand correctly, Haskell does not have subtypes in the way that F# does. Thus I expect that it does not have a type-test pattern for matching, like F#. Does it have any analogous constructs that can be used for metaprogramming?
I have an F# project in which I have decoupled modules that talk through a messaging mechanism. Recently I have been wondering what this code would look like, or if it would even be possible to write it this way, if I were to port it to Haskell.
The basic idea is this. A message is a type that inherits from the message interface:
type Message = interface end
A handler is a function that takes a specific subtype of Message
and returns a Message
:
type Handle<'TMsg when 'TMsg :> Message> = 'TMsg -> Message
There is a Bus
with a publish
method that distributes a message to its internal channels. There is one Channel<'TMsg>
per message type, and these are added dynamically when a handler is registered. The bus publishes all messages to all channels, and if it is of the wrong type, the channel simply returns an empty sequence:
type Channel<'TIn when 'TIn :> Message>(...) = ...
interface Channel with
member x.publish (message : Message) =
match message with
| :? 'TIn as msg ->
Seq.map (fun handle -> handle msg) _handlers
|> Seq.filter (fun msg -> msg <> noMessage)
| _ -> Seq.empty
Ultimately what I'm doing here is dynamic metaprogramming which allows me to have strongly typed messages that still get passed through the same machinery in the middle. I am not as up on Haskell as F#, and I can't figure out how to do this in Haskell.
Am I right that Haskell has no match... with... :?... as...
construct? Does it have a similar construct or another approach to this sort of metaprogramming that you can solve this sort of problem with? Is there some sort of box/unbox mechanism?
Haskell is designed so that types can be completely erased at runtime. That is, when an implementation stores a value of a type T
in memory, there is no need to tag it with some label saying "this is of type T
".
This gives nice parametricity guarantees in return, also referred to as "free theorems". For instance, a function having a polymorphic type
f :: a -> a
must return its argument or fail to terminate. That is, we know that f=id
(if we know it terminates).
Concretely, this implies that there's no way to write something like
f :: a -> a
f x = if a == Int then x+1 else x
If one wishes to do such a thing, one can manually add type tags through the Data.Typeable
class:
{-# LANGUAGE ScopedTypeVariables, GADTs, TypeOperators #-}
import Data.Typeable
f :: forall a . Typeable a => a -> a
f x = case eqT :: Maybe (a :~: Int) of
Just Refl -> x+1
Nothing -> x
Note how the type changed, and now has a constraint. It has to, since the effect of the function violates the free theorem of the unconstrained polymorphic type.
So, if you really need to perform runtime type checks, carry around a Typeable
constraint. Note that this is possibly too much general. If you know you have a small amount of types, probably you can use a sum type instead, and use plain pattern matching on constructors to test for the type:
data T = TInt Int
| TChar Char
f :: T -> T
f (TInt i) = TInt (i+1)
f (TChar c) = TChar c
Apologies for answering your question with Mu, but I wouldn't be doing it like that in F#, either.
That Message
interface is what's known as a Marker Interface, and while this is contentious, I consider it a code smell in any language that supports annotations (attributes, in .NET).
A Marker Interface doesn't do anything, so you can achieve the same behaviour without it.
I wouldn't design a messaging system like that in C#, because the Marker Interface adds nothing, except the illusion that a message is somehow 'typed'.
I wouldn't design a messaging system like that in F#, because there's a better, safer, strongly typed alternative in F#:
type UserEvent =
| UserCreated of UserCreated
| EmailVerified of EmailVerified
| EmailChanged of EmailChanged
This specifies clearly that the set of events is finite, and well-known. With that, you get compile-time checking instead of run-time checking.
(See here for the full example.)
Such a Discriminated Union is easy to translate to Haskell:
data UserEvent = UserCreated UserCreatedData
| EmailVerified EmailVerifiedData
| EmailChanged EmailChangedData
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With