Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using makeLenses, class constraints and type synonyms together

Tags:

haskell

I'm quite new to Haskell and want to use makeLenses from Control.Lens and class constraints together with type synonyms to make my functions types more compact (readable?).

I've tried to come up with a minimal dummy example to demonstrate what I want to achieve and the example serves no other purpose than this.

At the end of this post I've added an example closer to my original problem if you are interested in the context.

Minimal example

As an example, say I define the following data type:

data State a = State { _a :: a
                     } deriving Show     

, for which I also make lenses:

makeLenses ''State

In order to enforce a class constraint on the type parameter a used by the type constructor State I use a smart constructor:

mkState :: (Num a) =>  a -> State a
mkState n = State {_a = n}

Next, say I have a number of functions with type signatures similar to this:

doStuff :: Num a => State a -> State a
doStuff s = s & a %~ (*2)

This all works as intended, for example:

test = doStuff . mkState $ 5.5 -- results in State {_a = 11.0}

Problem

I've tried to use the following type synonym:

type S = (Num n) => State n -- Requires the RankNTypes extensions

, together with:

{-# LANGUAGE RankNTypes #-}

, in an attempt to simplify the type signature of doStuff:

doStuff :: S -> S

, but this gives the following error:

Couldn't match type `State a0' with `forall n. Num n => State n'
    Expected type: a0 -> S
    Actual type: a0 -> State a0
In the second argument of `(.)', namely `mkState'
    In the expression: doStuff . mkState
    In the expression: doStuff . mkState $ 5.5
Failed, modules loaded: none.

Question

My current knowledge of Haskell is not sufficient to understand what causes the above error. I hope someone can explain what causes the error and/or suggest other ways to construct the type synonym or why such a type synonym is not possible.

Background

My original problem looks closer to this:

data State r = State { _time :: Int
                     , _ready :: r
                     } deriving Show

makeLenses ''State

data Task = Task { ... }

Here I want to enforce the type of _ready being an instance of the Queue class using the following smart constructor:

mkState :: (Queue q) => Int -> q Task -> State (q Task)
mkState t q  = State { _time = t
                     , _ready = q
                     }

I also have a number of functions with type signatures similar to this:

updateState :: Queue q => State (q Task) -> Task -> State (q Task)
updateState s x = s & ready %~ (enqueue x) & time %~ (+1)

I would like to use a type synonym S to be able to rewrite the type of such functions as:

updateState :: S -> Task -> S

, but as with the first minimal example I don't know how to define the type synonym S or whether it is possible at all.

Maybe there is no real benefit in trying to simplify the type signatures?

Related reading

I've read the following related questions on SO:

  • Class constraints for data records
  • Are type synonyms with typeclass constraints possible?

This might also be related but given my current understanding of Haskell I cannot really understand all of it:

  • Unifying associated type synonyms with class constraints

Follow-up

It's been a while since I've had the opportunity to do some Haskell. Thanks to @bheklilr I've now managed to introduce a type synonym only to hit the next type error I'm still not able to understand. I've posted the following follow-up question Type synonym causes type error regarding the new type error.

like image 501
Karl Marklund Avatar asked Jun 01 '15 20:06

Karl Marklund


1 Answers

You see that error in particular because of the combination of the . operator and your use of RankNTypes. If you change it from

test = doStuff . mkState $ 5.5

to

test = doStuff $ mkState 5.5

or even

test = doStuff (mkState 5.5)

it will compile. Why is this? Look at the types:

doStuff :: forall n. Num n => State n -> State n
mkState :: Num n => n -> State n

(doStuff) . (mkState) <===> (forall n. Num n => State n -> State n) . (Num n => n -> State n)

Hopefully the parentheses help make it clear here, the n from forall n. Num n ... for doStuff is a different type variable from the Num n => ... for mkState because the scope of the forall only extends to the end of the parentheses. So these functions can't actually compose because the compiler sees them as separate types! There are actually special rules for the $ operator specifically for using the ST monad precisely for this reason, just so you can do runST $ do ....

You may be able to accomplish what you want easier using GADTs, but I don't believe lens' TemplateHaskell will work with GADT types. However, you can write your own pretty easily in this case, so it isn't that big of a deal.


A further explanation:

doStuff . mkState $ 5.5

is very different than

doStuff $ mkState 5.5

In the first one, doStuff says that for all Num types n, its type is State n -> State n, whereas mkState says for some Num type m, its type is m -> State m. These two types are not the same because of the "for all" and "for some" quantifications (hence ExistentialQuantification), since composing them would mean that for some Num m you can produce all Num n.

In the doStuff $ mkState 5.5, you have the equivalent of

(forall n. Num n => State n -> State n) $ (Num m => State m)

Notice that the type after the $ is not a function because mkState 5.5 is fully applied. So this works because for all Num n you can do State n -> State n, and you're providing it some Num m => State m. This works intuitively. Again, the difference here is the composition versus application. You can't compose a function that works on some types with a function that works on all types, but you can pass a value to a function that works on all types ("all types" here meaning forall n. Num n => n).

like image 178
bheklilr Avatar answered Nov 18 '22 09:11

bheklilr