1

Let's assume four functions f1,f2,f3,f4 :: SomeMonad m => m a -> m a where f1 and f2 are conceptually related and similarly f3 and f4.

Now it is possible to write a function f :: SomeMonad m => (m a -> m a) -> m a that can take any of the f1..f4 as input. And then it is possible to write, say, f1 $ f f2.

The question is, how to write a function g such that it accepts only f1 or f2 as a parameter? We should be able to write f4 $ g f1 and ghc should deny g f3.

If I try to add a class, say :: (SomeMonad m, Some_G_class a) =>, the return type a is also constrained and then f4 cannot be applied. And similarly it seems that the m cannot be modified.


Late Addition

The answers by Daniel Wagner and leftroundabout both seem to be useable.

Here is more about the actual (and practical) problem.

  • The underlying lib contains tens of functions returning m a.
  • The lib has been build so that functions can be chained, e.g. with (.) like f1 . f2 . f3.
  • Conceptually, there is about five to ten subsets of those functions, subsets overlapping. The functions should be chaineable within each subset but not with functions from the other subsets.

Using Proxies

It seems that

data Type = A | B | C ... | H
data Proxy (ty :: Type) = Proxy

could be used to classify functions. To handle overlapping useage, a function call like

h1 :: SomeMonad t m => Either (Proxy A) (Proxy B) -> m t -> m t
h1 (Left (Proxy :: Proxy A) mt = ..
h1 (Right (Proxy :: Proxy B) mt = ..

could do it. But what if there are functions belonging to A, B, C and D? Maybe there is something like OneOf or Any that would work here?

Using RankNTypes

It seems that the SpecificMonad-constraint (restriction?) starts to propage to the usage sites of the functions f3 and f4 (that is, all over). I feel that if I try to manage the 5-10 overlapping function sets, there will be serious difficulties with those constraints propagating every where. Is this the case?

So we need a function to make coercing, maybe something like

fg :: (SomeMonad t m1, SpecificMonad t m2) => m2 t -> m1 t
fg a = pure $ fromSpecM a

and atm assuming SomeMonad has pure. In my case I cannot add fromSpecM to the class-methods of SomeMonad but maybe it is ok in SpecificMonad. And my first trials along this way are still too simple so that I could see if this is doable. It may well be that I'm not seeing something very obvious here.

In a way, SpecificMonad shouldn't have any additional methods and it should just work as a "local env/fix/hack" not affecting other parts of the code. Thus, is there other ways to accomplish something like fg?

3
  • I think that in Haskell functions should be seen as blackboxes. Since there are a zillion ways to construct exactly the same function (partial application, lambda expressions, etc.). So it looks to me it would be bad design to do such "whitelisting". Commented Dec 12, 2017 at 9:42
  • In my case the whitelisting could be used to add some guarantees that are now left to the user of a lib. There are many conceptually overlapping sets of functions and thus the situation is a bit more complex than in the q above. In practice, I cannot change the lib that allows "too many usages for functions" to construct conceptually in-approriate function applications. But maybe a wrapper for the lib that would define appropriate data-types that can be "classified" and that would have a function producing m a. Commented Dec 12, 2017 at 10:53
  • 2
    Haskell does not have dependent types, hence substituting a subexpression like f1 with another one of the same type like f4 is always possible. You need to assign those two functions different types, e.g. using a newtype wrapper. Commented Dec 12, 2017 at 11:15

2 Answers 2

3

If f1,f2,f3,f4 all have the same type signature, then the type system can't distinguish between them (duh), so you also can't restrict which of them should be valid.

So before you can possibly accomplish your goal, you need some distinction in the signatures. Probably the most sensible way is to refine the monad-class constraint:

class SomeMonad m => SpecificMonad m

f1, f2 :: SomeMonad m => m a -> m a     -- same as before
f3, f4 :: SpecificMonad m => m a -> m a -- more restrictive

(You wouldn't need to change the implementations here, because SpecificMonad guarantees SomeMonad.)

Now, f :: SomeMonad m => (m a -> m a) -> m a by itself does not enforce that the argument fulfills any particular constraint – whatever function you pass in would merely add its constraint to the SomeMonad m constraint that f brings itself, i.e.

f f1 :: SomeMonad m => m a
f f3 :: SpecificMonad m => m a -- because `SomeMonad` is superclass, we don't need
                               -- to mention it, but it's actually implicit constraint

are both well-typed.

You can however enfore that f will only accept a function with a particular, not too specific constraint. That requires Rank-2 polymorphism:

{-# LANGUAGE RankNTypes, UnicodeSyntax #-}

g :: SomeMonad m => (∀ μ . SomeMonad μ => μ a -> μ a) -> m a
g φ = f φ

Now, you can write g f1, because f1 is a polymorphic function that works on any SomeMonad. You cannot however write g f3, because f3 is too picky in what monads it can operate with, and even if the outer m that g f3 is supposed to be used with does fulfill the SpecificMonad constraint, g will not allow its argument to access that information.


Of course, to now use f3 and f4 in any other, legitimate setting, such as f4 $ g f1, you will have to add instances for SpecificMonad to any monad they should work with. These are one-liners like

instance SpecificMonad []
instance SpecificMonad Maybe

(provided that these have SomeMonad instances already).


Written without Unicode:

g :: SomeMonad m => (forall m' . SomeMonad m' => m' a -> m' a) -> m a
g = f

As it must, if you want to write f4 $ g f1.

Sign up to request clarification or add additional context in comments.

1 Comment

This raised an extra q about the constraints induced by writing things like f3 in your answer. I made a class Hmm and class SHmm to represent envs SomeMonad and SpecificMonad, and then wrote functions f, g, f1,.., f4 to check types with ghci. The type of f4 $ g f1 will be specific (it has constraint SHmm). I'm suspecting that in practice I have to be able to turn SHmm back to Hmm (because otherwise I should put SHmm into quite many places). In small setting this might do but I'm suspecting that this doesn't scale to the problem I'm facing (I'll update the q a bit).
1

Perhaps you can get away with a simple phantom type parameter. For example:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}

data Type = A | B
data Id (ty :: Type) a = Id a

f1, f2 :: Id A a -> Id A a
f1 = id
f2 = id

f3, f4 :: Id B a -> Id B a
f3 = id
f4 = id

In ghci, we can see that f1 and f2 combine just fine, as do f3 and f4, but not e.g. f1 and f3:

Main> :t f1 . f2
f1 . f2 :: Id 'A a -> Id 'A a
Main> :t f3 . f4
f3 . f4 :: Id 'B a -> Id 'B a
Main> :t f1 . f3

<interactive>:1:6: error:
    • Couldn't match type ‘'B’ with ‘'A’
      Expected type: Id 'B a -> Id 'A a
        Actual type: Id 'B a -> Id 'B a
    • In the second argument of ‘(.)’, namely ‘f3’
      In the expression: f1 . f3

1 Comment

I made data Proxy (ty :: Type) a = Proxy and this made it possible to write f and g variants that work in the setting given in q above. And h1 :: SomeMonad t m => Proxy A -> m t -> m t etc. Scaling is something that is making my head itch. (I'll make a late addition to the q about scaling in a more complex/larger env that is closer to the actual practical problem I'm facing.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.