I am attempting to tackle this exercise from chapter 7 in a more generic way.
(Easy) Use lift2
to write lifted versions of the numeric operators +
, -
, *
and /
which work with optional arguments.
Here’s one solution:
addApply :: forall f a. Apply f => Semiring a => f a -> f a -> f a
addApply = lift2 add
subApply :: forall f a. Apply f => Ring a => f a -> f a -> f a
subApply = lift2 sub
-- etc.
I’d like to find a solution using type class instances, so I can use the default add
instead of my custom addApply
.
Here’s an attempt which produces an orphan instance error. I believe I need to create a type for (f a)
, but I’m failing to find the correct syntax.
instance semiringApply :: (Apply f, Semiring a) => Semiring (f a) where
add = lift2 add
mul = lift2 mul
zero = apply zero
one = apply one
That should be possible; I think the type you want is this:
newtype LiftedSemiring f a = LiftedSemiring (f a)
Full answer hidden under this details
tag (spoilers):
instance semiringLifted :: (Apply f, Semiring a) => LiftedSemiring (f a) where
add (LiftedSemiring x) (LiftedSemiring y) = LiftedSemiring (lift2 add x y)
mul (LiftedSemiring x) (LiftedSemiring y) = LiftedSemiring (lift2 mul x y)add
zero = LiftedSemiring (pure zero)
one = LiftedSemiring (pure one)
However, it’s worth noting that such an instance will not be law-abiding. For example, if we choose Maybe
for f
, then our zero
will be Just 0
. However, Just 0 * Nothing
would evaluate to Nothing
, which fails to satisfy the annihilation law (that anything times zero
is zero
).
2 Likes
Thanks for the guidance.
Is (f a)
in LiftedSemiring (f a)
considered a single argument? I assumed f
would appear as an additional constructor and not be allowed according to the docs which say that newtype is “restricted to a single constructor which contains a single argument”.
Now add
works if the arguments are wrapped by LiftedSemiring
. Is it possible to use add
without wrapping and without creating a new instance for each Apply
type?
-- This works
> :t LiftedSemiring (Just 1) + LiftedSemiring (Just 2)
LiftedSemiring Maybe Int
- This works
> :t Just 1 `addApply` Just 2
Maybe Int
-- Ideal behavior
> :t Just 1 + Just 2
Maybe Int
Here’s the snippet I’m using, which contains a few adjustments, since the original did not compile for me:
newtype LiftedSemiring f a = LiftedSemiring (f a)
instance semiringLifted :: (Applicative f, Semiring a) => Semiring (LiftedSemiring f a) where
add (LiftedSemiring x) (LiftedSemiring y) = LiftedSemiring (lift2 add x y)
mul (LiftedSemiring x) (LiftedSemiring y) = LiftedSemiring (lift2 mul x y)
zero = LiftedSemiring (pure zero)
one = LiftedSemiring (pure one)
addApply :: forall f a. Apply f => Semiring a => f a -> f a -> f a
addApply = lift2 add
That’s an interesting point about whether Just 0 * Nothing
should produce Just 0
or Nothing
. It seems like there should be a counterpart to the annihilation law for side effects where any operation involving side effects should preserve those side effects. I guess laws shouldn’t have exceptions, but I suspect it’s safer to prioritize tracking side effects over zeros.
Oops, yes, I didn’t bother putting mine in front of the compiler
The (f a)
in LiftedSemiring (f a)
is indeed a single argument, yes. If you wrote newtype T = T (Maybe Int)
, that would also be acceptable, for the same reason.
There’s no way of being able to write Just 1 + Just 2
, at least not unless you define your own version of +
, because you’d have to write an orphan instance to do that.
I’d suggest the following instead:
liftedAdd :: forall f a. Apply f => Semiring a => f a -> f a -> f a
liftedAdd = lift2 add
infixl 6 liftedAdd as ^+
I think the numerical hierarchy is best left as-is rather than weakening laws to support more instances. At the moment, the hierarchy is all based on well understood mathematical abstractions; if we were to relax the laws so that instances like Maybe Int
were permissible, the class would no longer represent a semiring (or a ring, or field, or whatever), and so you’d no longer be able to use existing results which do concern semirings or rings or fields. Additionally the example about Just 0 * Nothing = Nothing
is just the first law failure which came to mind; I suspect there are quite a few more, especially if you look to more applicatives than just Maybe.
1 Like