Context: I ran into a problem and got help here, however I realized I don't understand the solution.
The keyword appears to be polymorphic recursion. I thought that foldr
is polymorphic and recursive, but this seems to be a different concept.
This explicit recursion works (based on shell-monad):
cmdList :: (Param command, Param arg, CmdParams result) =>
command -> [arg] -> result
cmdList command = go . reverse where
go :: (Param arg, CmdParams result) => [arg] -> result
go [] = cmd command
go (arg:args) = go args arg
But trying to refactor the answer towards my original attempts using foldr
I quickly ran into the problem I struggled with initially: infinite types. Replacing a constant cmd command
with b
:
go :: (Param arg, CmdParams result) => result -> [arg] -> result
go b [] = b
go b (arg:args) = go b args arg
• Occurs check: cannot construct the infinite type:
result ~ arg -> result
• In the expression: go b args arg
It seems to me that GHC no longer accepts polymorphic result
that is either a value or a function.
Why does that happen?
Can I not use foldr
in such cases?
If a recursion never reaches a base case, it will go on making recursive calls forever and the program will never terminate. This is known as infinite recursion, and it is generally not considered a good idea. In most programming environments, a program with an infinite recursion will not really run forever.
In computer science, polymorphic recursion (also referred to as Milner–Mycroft typability or the Milner–Mycroft calculus) refers to a recursive parametrically polymorphic function where the type parameter changes with each recursive invocation made, instead of staying constant.
Here's a simple recursive function:
ignore :: [a] -> ()
ignore [] = ()
ignore (a:as) = ignore as
It is polymorphic, and it is recursive. But, like almost all recursions, whatever type the caller chooses for a
, the recursion happens using the same choice for a
. That is, we could write this, with ScopedTypeVariables and probably a handful of other extensions:
ignore :: forall a. [a] -> ()
ignore (a:as) = ignore @a as
The term "polymorphic recursion" refers to a very special kind of recursion, where the recursive call chooses a different type for some parameter. The standard example is this one:
data BalancedTree a = Leaf a | Branch (BalancedTree (a, a))
foldBT :: (a -> a -> a) -> BalancedTree a -> a
foldBT f (Leaf a) = a
foldBT f (Branch bt) = uncurry f $ foldBT (\(x, y) (x', y') -> (f x x', f y y')) bt
(For example, foldBT (+)
would add all the elements of the tree.)
The special thing here is that the recursive call to foldBT
is not recursing over a BalancedTree a
-- but over a BalancedTree (a, a)
instead. The type has changed, like this:
foldBT :: forall a. (a -> a -> a) -> BalancedTree a -> a
foldBT f (Branch bt) = uncurry f $ foldBT @(a, a) {- N.B. not a! -} (\(x, y) (x', y') -> (f x x', f y y')) bt
It turns out that type inference in the setting that allows this kind of thing is pretty hard. To deal with that, Haskell has a restriction: inferred types always use monomorphic recursion, that is, all recursive calls choose all the same values for the type variables involved. You can still get polymorphic recursion by giving an appropriate type signature, because then inference is not needed.
Now, in your working situation, you write
cmdList :: forall command arg result.
(Param command, Param arg, CmdParams result) =>
command -> [arg] -> result
cmdList command = go . reverse where
-- for clarity, I have chosen fresh type variable names
go :: forall arg' result'. (Param arg', CmdParams result') => [arg'] -> result'
go [] = cmd command
go (arg:args) = go args arg
Notice that in the recursive case, go
is being given an extra argument on the right hand side of the equation compared to the left! This is polymorphic recursion; the result
type is slowly accumulating extra arguments.
go (arg:args) = go @arg' @(arg' -> result') {- N.B. not result'! -} args arg
This means that in the base case,
go [] = cmd command
the type of cmd
may be different depending on how many arguments there are in the list! In other words, you can think of events happening in this order: first the user chooses a command type, an argument type, and a result type; then you traverse the list and choose a different result type that accepts more arguments; then you call cmd
with that newly chosen result type.
Compare the version that doesn't work:
go :: (Param arg, CmdParams result) => result -> [arg] -> result
go b [] = b
go b (arg:args) = go b args arg
In this version, a choice has been lost; first the user chooses a result type, then you traverse the list and choose the same result type as the user did; then you use their b
of that type. But this is a problem -- they may not have chosen the right number of arguments for that list, and even if they did, it's not really reasonable to expect the compiler to be able to know that statically!
This explains the occurs check error you've gotten: for go
to work right in this setting, its b
argument has to be able to accept an arbitrary number of arguments, and there just isn't any single type that does that.
It also explains why you cannot use foldr
: foldr
is implemented with monomorphic recursion. It is probably possible to create a variant of foldr
using RankNTypes that could sit in this spot, but I suspect it's more work than it's worth.
And, by the way, I think you can make your refactoring work, though I'm not 100% because I haven't installed shell-monad to check this. But it would look something like this:
go :: (Param arg, CmdParams result) => (forall result'. CmdParams result') -> [arg] -> result
-- same implementation as before
The key here is that putting the extra forall
there forces the first b
argument to be polymorphic; this lets you choose result'
to be a suitably-many-arguments variant of result
when you get to the base case.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With