Home > other >  How can I return a lambda with guards and double recursion?
How can I return a lambda with guards and double recursion?

Time:11-29

I made this function in python

def calc(a): return lambda op: {
    ' ': lambda b: calc(a b),
    '-': lambda b: calc(a-b),
    '=': a}[op]

So you can make a calculation like this

calc(1)(" ")(1)(10)("-")(7)("=")

And the result will be 5

I wanna make the same function in haskell to lean about lambdas - however I am getting parse errors:

My code looks like this:

calc :: Int -> (String -> Int)
calc a = \ op 
    | op == " " = \ b calc a b
    | op == "-" = \ b calc a b
    | op == "=" = a


main = calc 1 " " 1 " " 10 "-" 7 "="

CodePudding user response:

There are numerous syntactical problems with the code you have posted. I won't address them here, though: you will discover them yourself after going through a basic Haskell tutorial. Instead I'll focus on a more fundamental problem with the project, which is that the types don't really work out. Then I'll show a different approach that gets you the same outcome, to show you it is possible in Haskell once you've learned more.

While it's fine in Python to sometimes return an function-of-int and sometimes a string, this isn't allowed in Haskell. GHC has to know at compile time what type will be returned; you can't make that decision at runtime based on whether a string is "=" or not. So you need a different type for the "keep calcing" argument than the "give me the answer" argument.

This is possible in Haskell, and in fact is a technique with a lot of applications, but it's maybe not the best place for a beginner to start. You are inventing continuations. You want calc 5 plus 2 minus 1 equals to produce 6, for some definitions of the functions used therein. Achieving this requires some advanced features of the Haskell language and some funny types, which is why I say it is not for beginners. But, below is an implementation that meets this goal. I won't explain it in detail, because there is too much for you to learn first. Hopefully after some study of Haskell fundamentals, you can return to this interesting problem and understand my solution.

{-# LANGUAGE ExplicitForAll, RankNTypes #-}

type Calc a = forall r. (a -> r) -> r

calc :: a -> Calc a
calc x k = k x

equals :: a -> a
equals = id

lift2 :: (a -> a -> a) -> a -> a -> Calc a
lift2 f x y = calc (f x y)

plus :: Num a => a -> a -> Calc a
plus = lift2 ( )

minus :: Num a => a -> a -> Calc a
minus = lift2 (-)

ghci> calc 5 plus 2 minus 1 equals
6

CodePudding user response:

A Haskell function of type A -> B has to return a value of the fixed type B every time it's called (or fail to terminate, or throw an exception, but let's neglect that).

A Python function is not similarly constrained. The returned value can be anything, with no type constraints. As a simple example, consider:

def foo(b):
   if b:
      return 42        # int
   else:
      return "hello"   # str

In the Python code you posted, you exploit this feature to make calc(a)(op) to be either a function (a lambda) or an integer.

In Haskell we can't do that. This is to ensure that the code can be type checked at compile-time. If we write

bar :: String -> Int
bar s = foo (reverse (reverse s) == s)

the compiler can't be expected to verify that the argument always evaluates to True -- that would be undecidable, in general. The compiler merely requires that the type of foo is something like Bool -> Int. However, we can't assign that type to the definition of foo shown above.

So, what we can actually do in Haskell?

One option could be to abuse type classes. There is a way in Haskell to create a kind of "variadic" function exploiting some kind-of convoluted type class machinery. That would make

calc 1 " " 1 " " 10 "-" 7 :: Int

type-check and evaluate to the wanted result. I'm not attempting that: it's complex and "hackish", at least in my eye. This hack was used to implement printf in Haskell, and it's not pretty to read.

Another option is to create a custom data type and add some infix operator to the calling syntax. This also exploits some advanced feature of Haskell to make everything type check.

{-# LANGUAGE GADTs, FunctionalDependencies, TypeFamilies, FlexibleInstances #-}

data R t where
   I :: Int -> R String
   F :: (Int -> Int) -> R Int

instance Show (R String) where
    show (I i) = show i

type family Other a where
   Other String = Int
   Other Int    = String

(#) :: R a -> a -> R (Other a)
I i # " " = F (i )   -- equivalent to F (\x -> i   x)
I i # "-" = F (i-)   -- equivalent to F (\x -> i - x)
F f # i   = I (f i)
I _ # s   = error $ "unsupported operator "    s

main :: IO ()
main =
   print (I 1 # " " # 1 # " " # 10 # "-" # 7)

The last line prints 5 as expected.

The key ideas are:

  • The type R a represents an intermediate result, which can be an integer or a function. If it's an integer, we remember that the next thing in the line should be a string by making I i :: R String. If it's a function, we remember the next thing should be an integer by having F (\x -> ...) :: R Int.

  • The operator (#) takes an intermediate result of type R a, a next "thing" (int or string) to process of type a, and produces a value in the "other type" Other a. Here, Other a is defined as the type Int (respectively String) when a is String (resp. Int).

  • Related