5 ms·
> after all, you still need to test a condition for if/then/else, otherwise it is not if/then/else The way this works in Smalltalk is that primitives (e.g. equ
by nybble41 5y ago
> after all, you still need to test a condition for if/then/else, otherwise it is not if/then/else
The way this works in Smalltalk is that primitives (e.g. equality on numbers) return an object which is either `true` or `false`. These object each have their own implementations of methods such as `ifTrue:` and `ifFalse:` which accept code blocks. `true ifTrue: [ … ]` always runs the given code block, while `false ifTrue: [ … ]` never runs it. At a machine-code level the comparison primitives are most likely implemented with native conditions and branches, and the VM bytecode does include conditional branches for the sake of optimization, but in terms of the high-level language it's all based on the equivalent of C++ virtual method calls or C function pointers. Smalltalk has no syntax for conditions, or any syntactic equivalent of the C switch statement. (A dictionary or array of code blocks is typically used instead.)
Technically you can do without the comparison primitives if you encode your data in certain ways, e.g. using Peano numbers (and switching to Lambda Calculus):
false = λt. λf. f
true = λt. λf. t
zero = λf. λx. x
succ = λn. λf. λx. f (n f x)
one = succ zero
two = succ one
…
isZero = λn. n (λx. false) true
Code like `if (n == 0) { A } else { B }` would then become simply `(isZero n) A B`. Which gives you the same result without any language support for conditions, comparisons, or even booleans—just functions.
- auggierose 5y agoChurch numerals are nice, yes. Try executing 1000000 + 1000000 in Church numeral representation. Will you get a stack overflow?
- auggierose 5y agoThe following Swift code gives me a "Bad Access", already for 100000. Wouldn't it be nice if that could execute without problems on my 64GB RAM iMac Pro? import Foundation typealias T = () typealias ChurchNum = ((T) -> T, T) -> T func zero() -> ChurchNum { return { (f, x) in x } } func succ(_ g : @escaping ChurchNum) -> ChurchNum { return { (f, x) in f(g(f, x)) } } func add(_ g : @escaping ChurchNum, _ h : @escaping ChurchNum) -> ChurchNum { return { (f, x) in h(f, g(f, x)) } } func numeral(_ n : Int) -> ChurchNum { var z : ChurchNum = zero() for _ in 0 ..< n { z = succ(z) } return z } func realise(_ g : ChurchNum) -> Int { var n : Int = 0 func f(_ x : T) -> T { n += 1 return x } g(f, ()) return n } let x : ChurchNum = add(numeral(100000), numeral(100000)) print("x = \(realise(x))")
- nybble41 5y agoOn the other hand, this nearly identical program in Haskell runs just fine on my system in interpreted mode with a maximum resident size of about 160M, and less than 15M when compiled: {-# LANGUAGE RankNTypes #-} module Main where type ChurchNum = forall x. (x -> x) -> x -> x zero :: ChurchNum zero = \f x -> x succ' :: ChurchNum -> ChurchNum succ' g = \f x -> f (g f x) add :: ChurchNum -> ChurchNum -> ChurchNum add g h = \f x -> h f (g f x) numeral :: Int -> ChurchNum numeral 0 = zero numeral n = succ' (numeral (n - 1)) realize :: ChurchNum -> Int realize g = g (+1) 0 main :: IO () main = do let x = add (numeral 100000) (numeral 100000) putStrLn ("x = " <> show (realize x)) I must conclude that there is something wrong with either the Swift compiler or your environment for this to fail with "Bad Access" on such small inputs. The Haskell version works on sums up to at least 20M, though the memory requirements become somewhat alarming (1.3 GiB).
- auggierose 5y agoThat Haskell implements closures properly should come at no big surprise. I guess the problem is with Swift. I wonder what happens in Java with the same code. Or Kotlin. Or Typescript. I also wonder, what about recursion in Haskell? Is it limited by the stack size as in more conventional languages? Probably Haskell just allocates a very large stack size, and does it otherwise in the standard way. Otherwise it should be able to do much more than 20M. There is some talk about it here: https://gitlab.haskell.org/ghc/ghc/-/issues/8189 https://gitlab.haskell.org/ghc/ghc/-/issues/8189
- nybble41 5y agoHaskell has a traditional C-style stack, but it's usually very small (a few KiB). The evaluation model is very different from most other languages; recursive definitions do not necessarily result in recursive machine code, and almost all allocation is done on the heap. Certain rare forms of pathological code (not recursive definitions in general) can result in stack overflows. The cause is usually something like extensive recursion (which here includes iteration) without any other memory allocation or thunk evaluation. In any case, the point was not that we should all use recursion on Church-encoded Peano numbers for computation, since that is obviously very inefficient, but rather that tail recursion in particular—which does not result in excessive stack use, when implemented sanely—is a more general construct than iteration. It can be used to implement iteration, of course, but also other control flow patterns like state machines and coroutines which would otherwise require additional language support. The only major obstacle is poor language implementations, mostly for imperative languages (including Java), which insist on storing contextual data on the stack which is provably no longer needed by the program. Kotlin is a bit better but still hampered by the limitations of the JVM, so some kinds of tail calls (not direct recursion) still use stack space. EMCAScript 6 specifies guaranteed tail-call elimination, which should benefit Typescript, but Safari appears to be the only major environment which gets it right so far[0]. [0] https://kangax.github.io/compat-table/es6/#test-proper_tail_calls_(tail_call_optimisation) https://kangax.github.io/compat-table/es6/#test-proper_tail_...
- nybble41 5y ago> Try executing 1000000 + 1000000 in Church numeral representation. Will you get a stack overflow? The sum is just function composition, so not likely. In terms of using the sum, it depends on the function being applied and whether the evaluation model is lazy or strict. (And your stack size, of course.) The isZero function I gave above, for example, only needs to process the outermost "succ" or "zero" and the rest is ignored, which is perfect for lazy evaluation: isZero zero = (λn. n (λx. false) true) (λf. λx. x) -- def. of isZero and zero = (λf. λx. x) (λx. false) true -- β-reduce n = (λx. x) true -- β-reduce f = true -- β-reduce x isZero (succ N) -- for any N = (λn. n (λx. false) true) ((λn. λf. λx. f (n f x)) N) -- def. of isZero and succ = (λn. λf. λx. f (n f x)) N (λx. false) true -- β-reduce n = (λf. λx. f (N f x)) (λx. false) true -- β-reduce n = (λx. false) (N (λx. false) true) -- β-reduce f = false -- β-reduce x Of course, you can use other, more efficient representations in place of Peano numbers, such as Church-encoded lists of bits. Peano numbers are better suited for demonstrating the concept due to their simplicity. Lambda calculus doesn't really have a concept of "stack overflow" in any case. The point was that you can express both loops and conditions as well as more general structures such as state machines and continuations through (recursive) function calls, but—at least so far as I am aware—there is no way to similarly model data and generalized control-flow (other than loops, obviously) purely in terms of iteration. This is precisely because iteration constructs are more structured than recursion, and consequently have a narrow range of applications.
- auggierose 5y agoIf you want to model things, maybe use a logic instead. If you want to compute, then things like stack overflow matter very much. See my example in Swift. Is recursion helpful? Of course! Is tail recursion more useful than just the normal control structures like iteration that are already at your disposal? I doubt it, but as I said before, I wouldn't mind having properly implemented tail recursion at my disposal.