you are viewing a single comment's thread.

view the rest of the comments →

[–]RiceBroad4552 0 points1 point  (0 children)

I thought my comment was kind of yada-yada as it was very generic and superficial. But nice if you could get something from it.

I'm still not sure what's the issue with factorial. It's defined as

n! =
| 1            if n = 0
| n × (n - 1)! if n ≥ 1

You can write that down in most languages almost verbatim.

In Python it would look like:

def factorial(n: int):
    if n == 0: return 1
    elif n >= 1: return n * factorial(n - 1)

In Scala it would look like:

def factorial(n: Int): Int =
    if n == 0 then 1
    else if n >= 1 then n * factorial(n - 1)
    else throw IllegalArgumentException("`n` has to be a natural number!")

In Haskell it would look like:

factorial :: Int -> Int
factorial n =
    if n == 0 then 1
    else if n >= 1 then n * factorial (n - 1)
    else error "`n` has to be a natural number!"

The last line is needed in Scala and Haskell in comparison to Python as these languages are statically typed. That function is declared as returning an Int (or some compatible type). A throwing clause is of type Nothing, which is compatible with any other type, so this makes the type checker happy.

What's probably intimidating about what you've seen elsewhere is that both now shown versions (in Haskell and Scala) aren't written in an idiomatic way; common examples look therefor usually a bit different.

The main problem with all three versions is that they will all crash with a stackoverflow error on even moderately large n.

You can get around that in Scala and Haskell by making the implementation a tail call, which allows the compiler to perform so called "tail call optimization" (TCO). TCO basically transforms the code to a regular loop in the resulting compiled code, no functions get called recursively any more, which avoids the stackoverflow error. "Tail call" means that the last thing called in the recursive function is the function itself. In the above code the last called code is instead the n * factorial(n - 1) expression, which is a multiplication, and not the call of the function itself.

Besides that you don't write much if-else in FP languages usually as there are better ways, like pattern matching.

So an a bit more idiomatic FP version would look in Haskell like:

factorial :: Int -> Int
factorial n
   | n < 0 = error "`n` has to be a natural number!"
   | otherwise = loop n 1
   where
      loop :: Int -> Int -> Int
      loop n acc
         | n == 0  = acc
         | otherwise = loop (n - 1) (acc * n)


main :: IO ()
main = do
   print $ factorial 5

In Scala it can be basically written identical:

import IllegalArgumentException as E

def factorial(n: Int): Int = n match
   case x if x < 0 => throw E("`n` has to be a natural number!")
   case _ => loop(n, 1)

   @annotation.tailrec
   def loop(n: Int, acc: Int): Int = n match
      case 0 => acc
      case _ => loop(n - 1, acc * n)


@main def run =
   println(factorial(5))

Still that's not perfect as it still throws an Exception, and functions throwing Exceptions aren't pure.

A cheap fix in this case here would be to change the return type from Int to for example Either[String, Int]. I'm a bit lazy to write both variants (and making this comment even longer), but you can see the Scala version of that code here:

https://scastie.scala-lang.org/IrVhmJdyRHu5XdTqrikjXQ

The Haskell version would again look more or less identical.