1

I am using the hardware description tool Clash. Though this is a hardware description tool, my question is purely about Haskell.

There is a datatype of the form data Signal dom a = ... This datatype has an Applicative instance as follows:

instance Applicative (Signal domain) where
  pure  = signal#
  (<*>) = appSignal#

I have defined some functions which can sensibly take either Signal dom (Foo a b) or Foo a b. That is, they can either take a value wrapped in an Applicative, or they can take the value and then call pure to wrap it themselves.

I see two (ugly) ways to implement such a function:

  1. Create two versions of the function, one taking the "naked" value and the other taking the Applicative. Inside the first function, I call pure then delegate to the second function. These functions look something like the following:

    f :: (Foo a b) -> Signal dom Baz -> Signal dom Bar
    
    f = f' . pure
    
    f' :: Signal dom (Foo a b) -> Signal dom Baz -> Signal dom Bar
    
    f' = ...
    
  2. Create only one version of the function, then expect the user to call pure as needed.

I would like to create a typeclass to solve this, wherein the instance for Foo a b is pure and the instance for Signal dom (Foo a b) is id. But I cannot see any way to define such a thing.

Is there a way to define a typeclass, or is there another solution I have overlooked?

  • 3
    IMO, I'd use solution 2, unless in solution 1 somehow `f` can be implemented more efficiently than `f' . pure`. If I see both I start wondering why there is a strictly less general function as well, and only distracts. I would also avoid defining such a typeclass. IMO, these kinds of "magic" functions often lead to harder-to-understand type errors for users: we no longer know what is the expected type, and we need to search the docs for suitable instances. Also, even if it is possible, it looks like you need overlapping instances and those make instance selection more fragile. – chi Sep 21 '22 at 14:29
  • I think for my situation solution 1 is better than 2, because `f` is called very frequently (nearly every line in some of my files) and `f'` is called rarely. – Jonathan McDoe Sep 21 '22 at 14:44
  • 2
    Just to double-check: is there a sensible implementation of `fPure :: Foo a b -> Baz -> Bar`? If `f'` turns out to be `liftA2 fPure`, that seems like it would be a pretty good way to go. – Daniel Wagner Sep 21 '22 at 14:56
  • @DanielWagner There would be no sensible implementation. Besides, I don't see how that would solve the problem. It would be a more elegant internal representation but would expose a similar interface, would it not? – Jonathan McDoe Sep 21 '22 at 15:02
  • @JonathanMcDoe Let me give an analogy. Imagine that, instead of `Signal` we are talking about `[]` and your function just adds then doubles numbers in two lists. The function is (equivalent to) `f' = liftA2 (\x y -> (x + y) * 2)` and you say that it's annoying that you have two versions, the other version being `f x y = f' [x] [y]`. The better solution is just to have `g x y = (x + y) * 2` and, whenever you need to use it on lists, you can use `liftA2 g`. Here, `g` is the more broadly useful thing. If something like `g` is possible in the real example, I would recommend it for similar reasons. – David Young Sep 21 '22 at 22:24

1 Answers1

1

Of course if they can take an applicative then they can take a pure value. That's why we have pure. It may not be the most "fluent" thing but putting pure at the call site does have its advantages -- namely that the reader of the code knows that this value doesn't vary, and also that it is permitted to.

In any case, if the argument types are some fixed data type like Foo then you could:

class IsSignal dom a where
    type Arg dom a :: *
    toSignal :: a -> Signal dom (Arg dom a)

instance IsSignal dom (Foo a b) where
    type Arg dom (Foo a b) = Foo a b
    toSignal = pure
-- repeat this instance for every concrete type you expect

instance IsSignal dom (Signal dom a) where
    type Arg dom (Signal dom a) = a
    toSignal = id

f :: (IsSignal dom s, Arg dom s ~ Foo a b) => s -> Signal dom Baz -> Signal dom b
f a b = -- ... toSignal a ...

Whether that's worth it is up to you.

And if you want it to work on polymorphic types like

g :: a -> Signal dom Baz -> Signal dom a

There is, as far as I know, no good solution. There are things involving overlapping instances that may appear to work for the simplest examples, but they break very easily.

luqui
  • 59,485
  • 12
  • 145
  • 204
  • I have never encountered this technique of defining types within a class definition. I don't understand the semantics. Do you know of a document that describes it? – Jonathan McDoe Sep 22 '22 at 23:58
  • @JonathanMcDoe they're called [type families](https://wiki.haskell.org/GHC/Type_families) and they are pretty cool – luqui Sep 23 '22 at 17:55