Alecto
Alecto

Reputation: 10740

How can I write a function `run` that calls `runStateT` or `runReaderT`?

How can I write a generic function run that takes an object of some monad transformer, and calls the corresponding function?

Given run s,

I've tried creating a typeclass Runnable:

:set -XMultiParamTypeClasses
:set -XFlexibleInstances

class Runnable a b where
  run :: a -> b
  (//) :: a -> b
  (//) = run

instance Runnable (StateT s m a) (s -> m (a, s)) where
 run = runStateT

instance Runnable (ReaderT r m a) (r -> m a) where
 run = runReaderT

But when I try using run, it doesn't work. For example, let's define simpleReader, which just returns 10 when read:

simpleReader = ReaderT $ \env -> Just 10

runReaderT simpleReader ()

This outputs Just 10, like expected.

However, when I try using run, it gives me an error:

run simpleReader ()
<interactive>:1:1: error:
    • Non type-variable argument in the constraint: Runnable (ReaderT r Maybe a) (() -> t)
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        it :: forall r a t. (Runnable (ReaderT r Maybe a) (() -> t), Num a) => t

If I enable FlexibleContexts like it suggests, I get a different error:

<interactive>:1:1: error:
    • Could not deduce (Runnable (ReaderT r0 Maybe a0) (() -> t))
        (maybe you haven't applied a function to enough arguments?)
      from the context: (Runnable (ReaderT r Maybe a) (() -> t), Num a)
        bound by the inferred type for ‘it’:
                   forall r a t. (Runnable (ReaderT r Maybe a) (() -> t), Num a) => t
        at <interactive>:1:1-19
      The type variables ‘r0’, ‘a0’ are ambiguous
    • In the ambiguity check for the inferred type for ‘it’
      To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
      When checking the inferred type
        it :: forall r a t. (Runnable (ReaderT r Maybe a) (() -> t), Num a) => t

Upvotes: 2

Views: 375

Answers (2)

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

Short answer: you need a functional dependency on your class.

Long answer:

When the compiler sees run, it needs to find the appropriate instance of Runnable in order to determine which implementation of run to use. And in order to find that instance, it needs to know what a and b are. It knows that a is a ReaderT, so that one is covered. But what is b?

The compiler sees that you're using b as a function, passing () to it as argument. Therefore, thinks the compiler, b must be of shape () -> t, where t is not known yet.

And here is where it kinda stops: the compiler has nowhere to get t from, therefore it doesn't know b, therefore it can't find the appropriate instance, so kaboom!

But there is a remedy for this situation. If we look closer at what your Runnable class actually means, it's easy to see that b should be strictly defined by a. That is, if we know what the monad is, we know what the return value would be. Therefore, it should be possible for the compiler to determine b by knowing a. But alas, the compiler doesn't know that!

But there is a way to explain that to the compiler. It's called "functional dependency", and it's written like this:

class Runnable a b | a -> b where

This notation a -> b tells the compiler that b should be unambiguously determined by a. This will mean that, on one hand, the compiler won't let you define instances that violate this rule, and on the other hand, it will be able to find the appropriate instance of Runnable just by knowing a, and then determine b from that instance.

Upvotes: 6

HTNW
HTNW

Reputation: 29193

The output type of run is totally determined by the input type to it. Represent this as a functional dependency (-XFunctionalDependencies):

class Runnable a b | a -> b where
  run :: a -> b
  -- side note: (//) does not belong here
(//) :: Runnable a b => a -> b
(//) = run
-- instances as given

Now it works. The reason your version doesn't work is because there's no way to know what the output type should be. If you have e.g.

act :: ReaderT Env Identity Ret

Then

run act :: Runnable (ReaderT Env Identity Ret) b => b

And then we're stuck, there's no way to figure out what b should be. It's e.g. possible to add another instance

instance Runnable (ReaderT r m a) (ReaderT r m a) where
  run = id

And now run act can be either a ReaderT Env Identity Ret or Env -> Identity Ret. The dependency a -> b a) allows us to infer b from a and b) restricts instance declarations to make this possible. Conflicting instances like the one I gave are rejected, and run act has the b type inferred to Env -> Identity Ret by looking at the instance you gave, as desired.

Upvotes: 5

Related Questions