Language Design: Gotchas With Variadic Minus
Treating the minus operator as a function can be tricky and dangerous
published Oct 17 2020
-, as seen in Lisps, has gotchas; it may be allowed syntactically, but not as a variadic function.
- tends to be overloaded with two different operations: negation and subtraction. Negation is always unary. Subtraction can be variadic. Unary subtraction is an identity function that returns the first argument unchanged without negating it.
ƒ negate(a) = 0 - a ƒ subtract(a) = a ƒ subtract(a b) = a - b ƒ subtract(a b c) = (a - b) - c ƒ subtract(a b c d) = ((a - b) - c) - d
In math and many programming languages, there’s no ambiguity because
- is either unary prefix (negation) or binary infix (subtraction):
-A | Negation. B - C | Subtraction.
But in Lisps,
- is always prefix, always variadic, and when called with a single argument, it always negates it.
The following examples use Racket. Let’s dynamically pass N arguments to
#lang racket/base (define (subtract . args) (apply - args)) (println (subtract 11 33 55)) (println (subtract 11 33)) (println (subtract 11))
-77 -22 -11 ; Performed negation, not subtraction!
The last call performed negation on its only argument.
Correct variadic subtraction:
#lang racket/base (define (flip fun) (lambda (a b) (fun b a))) (define (foldl1 fun seq) (foldl fun (car seq) (cdr seq))) (define (subtract . args) (foldl1 (flip -) args)) (println (subtract 11 33 55)) (println (subtract 11 33)) (println (subtract 11))
-77 -22 11
11 was correctly returned as-is.
Worth comparing to Haskell, which also generalizes operators into functions, but handles
- differently. In Haskell, the function
- is always binary subtraction:
main = do print (foldl1 (-) [11, 33, 55]) print (foldl1 (-) [11, 33]) print (foldl1 (-) )
-77 -22 11
Haskell doesn’t allow to overload functions on parameter count. You can’t define
- as both unary and binary. So they special-cased unary
- in the syntax, converting it to
main = do print (-11) print (negate 11)
Lisp and Haskell create this problem for themselves by treating
- as a function while overloading it with two different functions. Most languages don’t have this problem because they don’t have
- as a function. Languages with operator overloading tend to differentiate between negation and subtraction. For example, Rust has
- is converted into calls to one of those. When passing it to a higher-order function, you either pass
ops::Sub::sub, avoiding the problem completely.