The Haskell one is super clever for a number of reasons!
-
Foremost, g() is not syntax for calling g with no arguments, it's syntax for calling g with a single argument: the empty tuple
-
There is no such thing as a polyvariadic (i.e. a function with "splat" arguments) function in Haskell! The type must indicate the exact set of arguments
-
Haskell is statically typed, so if you try to write a function which returns a function then the type would fix how many times you could apply an empty tuple. In other words, here's a simple function which returns g()()("al") --> "gooal"
g :: () -> (() -> (String -> String))
g = \() -> \() -> \string -> "goo" ++ string
if it's not clear, there's no room for this function to suddenly accept a new tuple argument.
So, this seems pretty terrible. How does the solution work?
"Simple." We teach the compiler how to generate, on the fly, every possible function:
g0(s) -- "g" ++ s
g1()(s) -- "go" ++ s
g2()()(s) -- "goo" ++ s
g3()()()(s) -- "gooo" ++ s
...
then we name them all g. At compile time Haskell wanders around looking for (ambiguous) references to g, infers their type, and then uses that inferred type to determine which of the gN-series of functions is correct!
Then it inlines that gN function, typechecks it to prove that everything is sane, and goes on its merry way.
I said that Haskell demands we give exact types to all functions. So what's the type of g?
g :: (Goaly a, Stringy b) => b -> a
Here, Goaly and Stringy are defined in eatnumber1's solution. Roughly, they indicate polymorphism: b can be either () or a string indicating the two kinds of input types we're interested in and a is either a String (if we're demanding the result) or a "continuation" function of type (Goaly a, Stringy b) => b -> a again.
Together these cases ensure that we've got an inductive definition of the entire family of gN functions. We've actually defined something even larger, to be honest:
>>> g()"ooooo"()"al" :: String
"goooooooal"
We can even add more cases
instance Stringy Int where
toString 0 = "o"
toString n = show n
(+++) x y = x ++ toString y
>>> default (Int)
>>> g()0()0()0()0()0()0"al" :: String
"gooooooooooooal"
The Haskell one is super clever for a number of reasons!
Foremost,
g()is not syntax for callinggwith no arguments, it's syntax for callinggwith a single argument: the empty tupleThere is no such thing as a polyvariadic (i.e. a function with "splat" arguments) function in Haskell! The type must indicate the exact set of arguments
Haskell is statically typed, so if you try to write a function which returns a function then the type would fix how many times you could apply an empty tuple. In other words, here's a simple function which returns
g()()("al") --> "gooal"if it's not clear, there's no room for this function to suddenly accept a new tuple argument.
So, this seems pretty terrible. How does the solution work?
"Simple." We teach the compiler how to generate, on the fly, every possible function:
then we name them all
g. At compile time Haskell wanders around looking for (ambiguous) references tog, infers their type, and then uses that inferred type to determine which of thegN-series of functions is correct!Then it inlines that
gNfunction, typechecks it to prove that everything is sane, and goes on its merry way.I said that Haskell demands we give exact types to all functions. So what's the type of
g?Here,
GoalyandStringyare defined ineatnumber1's solution. Roughly, they indicate polymorphism:bcan be either()or a string indicating the two kinds of input types we're interested in andais either a String (if we're demanding the result) or a "continuation" function of type(Goaly a, Stringy b) => b -> aagain.Together these cases ensure that we've got an inductive definition of the entire family of
gNfunctions. We've actually defined something even larger, to be honest:We can even add more cases