How to implement Factorial via Control.Arrow.loop?
Asked Answered
V

1

6

I wonder whether it is possible to implement factorial using Control.Arrow.loop.

loop :: ArrowLoop a => a (b, d) (c, d) -> a b c

One of the evident ideas is to implement a somehow terminating branch (a branch where the first element of the pair (type c) wouldn't depend on the second element of the pair (type d)). It seems to me that it can't be done since we can't apply any boolean function to the second element of the pair (type d) during the first iteration because it would cause infinite recursion, so it only leaves us with the argument (type b), but the result of any boolean function wouldn't differ depending on the iteration (the argument doesn't change), thus, it would either terminate instantly or never terminate at all. The other idea I had is to create an endless stream of factorials, but this doesn't seem real either, since, once again, the argument can't be changed. So, I have 3 questions:

  1. Am I right about the points above?
  2. Am I missing any other concept which would help to implement factorial via Control.Arrow.loop?
  3. What is the correct idea behind this implementation?
Vashtivashtia answered 31/8, 2019 at 21:46 Comment(7)
Remember: d can be a function typeBarogram
@Barogram True. I tried creating a composition of products with the id as the terminating value, yet, the problem with accumulation still holds. Or did you mean something else by that?Vashtivashtia
Using ArrowCircuit and newtype SF a b = SF { runSF :: [a] -> [b] } from “Programming with Arrows”, this could be written as fact = last . runSF factA . enumFromTo 1 . succ where factA = proc x -> do { rec { acc <- delay 1 -< x * acc }; returnA -< acc }, which I’m pretty sure does essentially the same thing as fact = product . enumFromTo 1 but with a bit more allocation.Calabria
@JonPurdy since Arrow.loop underlies rec, it means, it should be possible indeed. Now we simply need to rewrite the recursive notation, using Arrow.loop.Vashtivashtia
@ZhiltsoffIgor do you know how to write factorial using fix? BTW this can be done using just ArrowLoop, we don't actually require anything like delay.Barogram
@Barogram fact = fix $ \ f n -> if n > 1 then n * f (n - 1) else 1, as far as I can tell. I should’ve thought about it, right.Vashtivashtia
@ZhiltsoffIgor also, fact = fix (\ f acc i -> if i > 1 then f (i*acc) (i-1) else acc) 1.Apollus
P
5

I've never actually used ArrowLoop before, loop is pretty cool.

Here is a factorial implemented using loop:

fact :: Integer -> Integer
fact =
  loop $ \(n, f) ->
    ( f n 1
    , \i acc ->
        if i > 0
          then f (i - 1) (i * acc)
          else acc)

Let's give it a try:

λ> fact <$> [1..11]
[1,2,6,24,120,720,5040,40320,362880,3628800,39916800]

I don't know if I can answer the first question you have, but for the 3rd one it's obviously possible. For the concepts that could help you, I think the fix point is the one you are looking for. For example you can start by trying this ;)

λ> import Data.Function
λ> fix error

Once you press enough Ctrl+C you can write factorial using fix point:

λ> let fact = fix $ \ f i -> if i > 1 then i * f (i - 1) else i
λ> fact <$> [1..11]
[1,2,6,24,120,720,5040,40320,362880,3628800,39916800]

Edit

It seems like a bit of expansion on the answer could be helpful.

First of all let's look at an alternative and better (due to tail recursion) implementation of fact using fix, so we can see how it compares with our implementation using loop:

factFix :: Integer -> Integer
factFix n =
  fix
    (\f ->
       \i acc ->
         if i > 0
           then f (i - 1) (i * acc)
           else acc)
    n
    1

We can see it is not far off. In both cases we get f as an argument and we return back a function that uses that f, in fact, the returned non-recursive function is identical in both cases. Just for clarity let's extract it an reuse in both places:

factNoRec :: (Ord p, Num p) => (p -> p -> p) -> p -> p -> p
factNoRec f i acc =
  if i > 0
    then f (i - 1) (i * acc)
    else acc

factLoop :: Integer -> Integer
factLoop n = loop (\(k, f) -> (f k 1, factNoRec f)) n

factFix :: Integer -> Integer
factFix n = fix (\f -> factNoRec f) n 1

Hopefully now it is much more apparent that they are really related concepts.

Looking into implementations of fix and loop (at least for functions, cause there are also mfix and loop for Kleisli) provides even more insight into their relation:

λ> fix f = let x = f x in x
λ> loop f b = let (c,d) = f (b,d) in c

They are really close to each other.

How about type signatures:

λ> :t fix
fix :: (t -> t) -> t
λ> :t loop
loop :: ((b, d) -> (c, d)) -> b -> c

Those look different. But if you do a bit of unification in the fact case you'll see that fix and loop acquire types:

λ> :t fix :: ((a -> b -> c) -> (a -> b -> c)) -> a -> b -> c
λ> :t loop :: ((b, a -> b -> c) -> (c, a -> b -> c)) -> b -> c

All of a b and c become all Integer in the end, but looking at type variables instead gives a better insight into what's going on. And really what's going on is just recursion by the means of fixed point combinators.

Prolocutor answered 1/9, 2019 at 1:13 Comment(10)
Can you recommend something to read about the fixed point to get a better grasp of this concept, please?Vashtivashtia
I read about fixed points, and I can't see how they are used in your solution with loop. It seems that we simply bind the 2nd element of the pair (f) with some recursive definition which has a terminating branch and then we simply apply it to the argument, which gives us a result as the 1st element of the pair, which doesn't depend on the function anymore since the recursion has terminated. The Fibonacci sequence is pretty much the same: fib = loop $ \(n, f) -> (f n (0, 1), \i (p, q) -> if i == 0 then p else f (i - 1) (q, p + q)) Am I missing the point of fixed points?Vashtivashtia
@ZhiltsoffIgor, but the f we bind in the pair is precisely what makes the definition recursive -- this is the sense in which fixed points are used. When factorial recurses, it calls f (i - 1) (i * acc), not "itself". It's only the fixed point in loop which ties the knot, which makes f equal to \i acc -> ....Barogram
@ZhiltsoffIgor AFAICT, yes, you're right. looking at the loop source for the -> ArrowLoop instance, it is a simple let binding, and those are recursive in Haskell by themselves of course. So there's no explicit fix there, just fact n === loop (\(n, f) -> (f n 1, G[f])) n === let (c,d) = (\(n, f) -> (f n 1, G[f])) (n,d) in c === let (c,f) = (\(n, f) -> (f n 1, G[f])) (n,f) in c === let (c,f) = (f n 1, G[f]) in c === let f = G[f] in f n 1 where G[f] = \i acc -> ...f.....Apollus
@WillNess got it, thank you. Can you recommend any depository of problems of such sort to get a better understanding of the overall theme (or any other literature on that topic)?Vashtivashtia
@ZhiltsoffIgor sadly, no. do you mean fix, or loop? I know next to nothing about the latter. for the former, there's google, wikipedia, and y-combinator. :) ---- do notice that implicitly the fix is there, since it is defined as fix g = let {f = g f} in f and this is what we have there. also, have you seen this trick: (in pseudocode) let_rec fact = (\n -> ...fact...) ==> let_rec fact = (\f -> (\n -> ...f...)) fact ==> let_nonrec fact = fix (\f -> (\n -> ...f...)).Apollus
@WillNess, yes, I've seen a pretty interesting paper about such a trick. Maybe someone would also like to take a look, so here it is - vex.net/~trebla/haskell/fix.xhtml.Vashtivashtia
@ZhiltsoffIgor I expended the answer a bit with the hope that it will provide more intuition into what's going on. I don't know of any resources of top of my head, since I learned about this stuff back in college, therefore the answer above is just my understanding of this topic.Prolocutor
some (very) old lisp and ml stuff on Y is for instance dreamsongs.com/Files/WhyOfY.pdf, and lambda-the-ultimate.org/classic/message5463.html (the archived pdf of that article is linked here) (the last one I think also goes by the name of "open recursion" fwiw)Apollus
@ZhiltsoffIgor ah right, thanks for the link. I tried googling "could reinvent y combinator" but that found nothing pertinent.Apollus

© 2022 - 2024 — McMap. All rights reserved.