Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class contraints for monads and monad functions

Tags:

haskell

monads

I am trying to write a new monad that only can contain a Num. When it fails, it returns 0 much like the Maybe monad returns Nothing when it fails.

Here is what I have so far:

data (Num a) => IDnum a = IDnum a

instance Monad IDnum where
  return x = IDnum x
  IDnum x >>= f  = f x
  fail :: (Num a) => String -> IDnum a
  fail _ = return 0

Haskell is complaining that there is

No instance for (Num a) arising from a use of `IDnum'

It suggests that I add a add (Num a) to the context of the type signature for each of my monad functions, but I tried that it and then it complains that they need to work "forall" a.

Ex:

Method signature does not match class; it should be
  return :: forall a. a -> IDnum a
In the instance declaration for `Monad IDnum'

Does anyone know how to fix this?

like image 236
Ian Avatar asked Mar 11 '14 00:03

Ian


2 Answers

The existing Monad typeclass expects your type to work for every possible type argument. Consider Maybe: in Maybe a, a is not constrained at all. Basically you can't have a Monad with constraints.

This is a fundamental limitation of how the Monad class is defined—I don't know of any way to get around it without modifying that.

This is also a problem for defining Monad instances for other common types, like Set.

In practice, this restriction is actually pretty important. Consider that (normally) functions are not instances of Num. This means that we could not use your monad to contain a function! This really limits important operations like ap (<*> from Applicative), since that depends on a monad containing a function:

ap :: Monad m => m (a -> b) -> m a -> m b

Your monad would not support many common uses and idioms we've grown to expect from normal monads! This would rather limit its utility.

Also, as a side-note, you should generally avoid using fail. It doesn't really fit in with the Monad typeclass: it's more of a historic accident. Most people agree that you should avoid it in general: it was just a hack to deal with failed pattern matches in do-notation.

That said, looking at how to define a restricted monad class is a great exercise for understanding a few Haskell extensions and learning some intermediate/advanced Haskell.

Alternatives

With the downsides in mind, here are a couple of alternatives—replacements for the standard Monad class that do support restricted monads.

Constraint Kinds

I can think of a couple of possible alternatives. The most modern one would be taking advantage of the ConstraintKind extension in GHC, which lets you reify typeclass constraints as kinds. This blog post details how to implement a restricted monad using constraint kinds; once I've read it, I'll summarize it here.

The basic idea is simple: with ConstraintKind, we can turn our constrain (Num a) into a type. We can then have a new Monad class which contains this type as a member (just like return and fail are members) and allows use to overload the constraint with Num a. This is what the code looks like:

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeFamilies    #-}

module Main where

import Prelude hiding (Monad (..))

import GHC.Exts

class Monad m where
  type Restriction m a :: Constraint
  type Restriction m a = ()

  return :: Restriction m a => a -> m a
  (>>=)  :: Restriction m a => m a -> (a -> m b) -> m b
  fail   :: Restriction m a => String -> m a

data IDnum a = IDnum a

instance Monad IDnum where
  type Restriction IDnum a = Num a
  return        = IDnum
  IDnum x >>= f = f x
  fail _        = return 0

RMonad

There is an existing library on hackage called rmonad (for "restricted monad") which provides a more general typeclass. You could probably use this to write your desired monad instance. (I haven't used it myself, so it's a bit hard to say.)

It doesn't use the ConstraintKinds extension and (I believe) supports older versions of GHC. However, I think it's a bit ugly; I'm not sure that it's the best option any more.

Here's the code I came up with:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

import Prelude hiding (Monad (..))

import Control.RMonad
import Data.Suitable

data IDnum a = IDnum a

data instance Constraints IDnum a = Num a => IDnumConstraints
instance Num a => Suitable IDnum a where
  constraints = IDnumConstraints

instance RMonad IDnum where
  return        = IDnum
  IDnum x >>= f = f x
  fail _        = withResConstraints $ \ IDnumConstraints -> return 0

Further Reading

For more details, take a look at this SO question.

Oleg has an article about this pertaining specifically to the Set monad, which might be interesting: "How to restrict a monad without breaking it".

Finally, there are a couple of papers you could also read:

  • The Constrained-Monad Problem
  • Generic Monadic Constructs for Embedded Languages
like image 63
Tikhon Jelvis Avatar answered Nov 07 '22 02:11

Tikhon Jelvis


This answer will be brief, but here's another alternative to go along with Tikhon's. You can apply a codensity transformation to your type to basically get a free monad for it. Just use it (in the below code it's IDnumM) instead of your base type, then convert the final value to your base type at the end (in the below code, you would use runIDnumM). You can also inject your base type into the transformed type (in the below code, that would be toIDnumM).

A benefit of this approach is that it works with the standard Monad class.

data Num a => IDnum a = IDnum a

newtype IDnumM a = IDnumM { unIDnumM :: forall r. (a -> IDnum r) -> IDnum r }

runIDnumM :: Num a => IDnumM a -> IDnum a
runIDnumM (IDnumM n) = n IDnum

toIDnumM :: Num a => IDnum a -> IDnumM a
toIDnumM (IDnum x) = IDnumM $ \k -> k x

instance Monad IDnumM where
  return x = IDnumM $ \k -> k x
  IDnumM m >>= f = IDnumM $ \k -> m $ \x -> f x `unIDnumM` k
like image 37
Jake McArthur Avatar answered Nov 07 '22 00:11

Jake McArthur