A Curried Composition Puzzle
The post presents a puzzle on creating a three-fold composition function from a two-fold function in Lambda calculus, Haskell, Scheme, and JavaScript, emphasizing visualization and proposing further challenges.
Read original articleThe post discusses a puzzle related to function composition in functional programming, specifically focusing on creating a three-fold composition function using only a two-fold composition function. The puzzle is presented in four programming languages: Lambda calculus, Haskell, Scheme, and JavaScript. The author provides definitions for two-fold and three-fold composition functions and challenges readers to express the three-fold composition function using only the two-fold version. The author shares their thought process, which involved visualizing function evaluations as trees, making the problem more manageable. The solutions for each language are provided, demonstrating how to construct the three-fold composition function from the two-fold one. The author reflects on the unintuitive nature of the solution and expresses a desire to gain deeper insights into the problem. Additionally, the post concludes with two further challenges: generalizing the solution to n-fold composition and exploring the impossibility of writing a specific reverse-order composition function using only the two-fold composition function.
- The puzzle involves creating a three-fold composition function from a two-fold function.
- Solutions are provided in Lambda calculus, Haskell, Scheme, and JavaScript.
- The author emphasizes the utility of visualizing function evaluations as trees.
- The solution is described as unintuitive, prompting further exploration of the problem.
- Two additional challenges are proposed for readers to consider.
Related
Functional languages should be so much better at mutation than they are
The article explores integrating mutation into functional programming, discussing methods like restricted mutable structures, local mutation, and linearity, while highlighting challenges and the need for a balanced approach.
How to Compose Functions That Take Multiple Parameters: Epic Guide
James Sinclair discusses composing JavaScript functions with multiple parameters by transforming them into unary functions, using composite data structures, and employing techniques like partial application and currying for better reusability.
Lisp's Grandfather Paradox
The article explores primitive recursion's historical context, key contributors, foundational functions, limitations, and implications in programming languages, emphasizing experiential learning and philosophical connections to Lisp.
What's Functional Programming All About?
Functional programming is often misunderstood, focusing on irrelevant details. It emphasizes complexity control and clearer structures, contrasting with imperative programming's challenges, promoting better error handling and refactoring.
How to Compose Math Problems
The article highlights how composing math problems fosters creativity and critical thinking, emphasizing characteristics of "beautiful" problems and the importance of pattern recognition in problem formulation and discovery.
This theme shows up repeatedly in Dijkstra's work[1]. The symbol manipulation is itself a calculation.
As a final influence I must mention our desire to let the symbols do the work —more precisely: as much of the work as profitably possible—. The intuitive mathematician feels that he understands what he is talking about and uses formulae primarily to summarize situations and relations in to him familiar universes. When he seems to derive one formula from another, the transformations he allows are those that seem to be true in the universe he has in mind. The formalist, however, prefers to manipulate his formulae, temporarily ignoring all interpretations they might admit, the rules for the permissible symbol manipulations being formulated in terms of those symbols: the formalist calculates with uninterpreted formulae.
The diagrammatic solution the author uses is related to something else I've been interested in, namely Peirce's Existential Graphs[2]. They are also based on diagrammatic representations of mathematical objects with a small set (somewhere between 3 and 6 depending on how you count inversion) of permissible rewrites. Existential Graphs are powerful enough to represent propositional or predicate logic with the exact same rules of inference! There is also an extension for something equivalent to Tarski's modal logic, but I haven't looked into it really. Peirce also designed existential graphs to be amenable to uninterpreted manipulation.The tutorial I link has some nice examples of proving Frege's axioms using only a single assumed axiom and rules of inference he proved sound. Frege assumed all of his axioms. I mean no disrespect to Frege, but it illustrates just how powerful existential graphs are.
[1] https://www.cs.utexas.edu/~EWD/transcriptions/EWD13xx/EWD130...
[2] https://www.jfsowa.com/pubs/egtut.pdf or https://www.jfsowa.com/peirce/ms514.htm
(1 b) ~> λgfx.(g (f x))
(2 b) ~> λhgfx.(h (g (f x)))
(3 b) ~> λihgfx.(i (h (g (f x))))
...
Furthermore: (X (Y b)) = (X*Y b)
I use these in my bruijn programming language in the form of infix/prefix operators. [1][1] https://bruijn.marvinborner.de/std/Combinator.bruijn.html#b
A cute alternative expression to solve the curried composition puzzle is c c c c c, just 5 c's in a row :)
Finally, Lean is a great language to do these puzzles in. See the following code:
/-- Compose! -/
def c (g : β → γ) (f : α → β) := g ∘ f
/-
?f (?g (?h ?x))
-/
#reduce c (c c) c ?f ?g ?h ?x
/-
?f (?g (?h ?x))
-/
#reduce c c c c c ?f ?g ?h ?x
My steps:
c(g): (f) => (x) => g(f(x))
c(c): (f) => (x) => c(f(x)) = (g) => (y) => f(x)(g(y)), or
(f) => (x) => (g) => (y) => f(x)(g(y))
c(c)(c): (x) => c(c(x)) = (f) => (y) => c(x)(f(y)) = (z) => x(f(y)(z)), or
(x) => (f) => (y) => (z) => x(f(y)(z)) # close!
c(c(c)): (f) => (x) => c(c)(f(x)) = (y) => c(f(x)(y)) = (g) => (z) => f(x)(y)(g(z)), or
(f) => (x) => (y) => (g) => (z) => f(x)(y)(g(z)) # We just need to substitute f with c
c(c(c))(c): (h) => c(c)(c(h)) = (g) => c(c(h)(g)) = (f) => (x) => c(h)(g)(f(x)) = h(g(f(x)), or
(h) => (g) => (f) => (x) => h(g(f(x)))
It’s really fun to puzzle out combinators made from smaller combinators, but it doesn’t take me long to find myself in the tall grass. SKI calculus goes from 0 to insane so quickly!
The famous Smullyan's book To Mock a Mockingbird is full of little puzzles like this.
Start with some definitions:
c :: (b -> c) -> (a -> b) -> (a -> c)
c = \g f x -> g (f x) -- alternatively, c = (.)
answer :: (y -> z) -> (x -> y) -> (w -> x) -> (w -> z)
answer = undefined
We need to start somewhere: answer = c
- Couldn't match type ‘x’ with ‘w -> x’
Expected: (y -> z) -> (x -> y) -> (w -> x) -> w -> z
Actual: (y -> z) -> (x -> y) -> x -> z
Okay, we knew this wouldn't work, does a solution exist if we apply `c` to a single argument? answer = c _1
- Couldn't match type ‘y’ with ‘x -> y’
Expected: (y -> z) -> (x -> y) -> (w -> x) -> w -> z
Actual: (y -> z) -> y -> (w -> x) -> w -> z
- Found hole: _1 :: z -> (w -> x) -> w -> z
Nope, it can't: `c` allows its 3rd argument to be an arbitrary type `y`, but we know the corresponding argument to `answer` should be constrained to a function type `x -> y`. So we need to fully apply `c`. answer = c _1 _2
- Found hole: _1 :: b0 -> (x -> y) -> (w -> x) -> w -> z
Where: ‘b0’ is an ambiguous type variable
- Found hole: _2 :: (y -> z) -> b0
Where: ‘b0’ is an ambiguous type variable
Okay, great, we'll be done if we can find values for our typed holes. Again, we need to start somewhere: answer = c c _2
- Couldn't match type ‘x’ with ‘w -> x’
Expected: (y -> w -> z) -> (x -> y) -> (w -> x) -> w -> z
Actual: (y -> w -> z) -> (x -> y) -> x -> w -> z
- Found hole: _2 :: (y -> z) -> y -> w -> z
Again, the shape of [the 2nd] `c` is wrong here: we need a function whose 3rd argument is a function of some sort, not an arbitrary type (NB in retrospect we could've realized this by looking at the shape of `_1`'s type). Let's try the simplest possible option: answer = c (c _3) _2
- Found hole: _3 :: b0 -> (w -> x) -> w -> z
Where: ‘b0’ is an ambiguous type variable
- Found hole: _2 :: (y -> z) -> (x -> y) -> b0
Where: ‘b0’ is an ambiguous type variable
We seem to be on the right track here: the 2nd `c` now typechecks. Even better, `_3` has a familiar looking type: it looks suspiciously similar to the type of `c`. Let's see if that works. answer = c (c c) _2
- Found hole: _2 :: (y -> z) -> (x -> y) -> x -> z
Where: ‘y’, ‘x’, ‘z’ are rigid type variables bound by
The final piece of the puzzle is now hopefully obvious: answer = c (c c) c
Much like the article's author (which I read after finding my own solution), I don't feel like I gained any great insight through this process, other than how useful it is to have good compiler error messages alongside features like typed holes.Related
Functional languages should be so much better at mutation than they are
The article explores integrating mutation into functional programming, discussing methods like restricted mutable structures, local mutation, and linearity, while highlighting challenges and the need for a balanced approach.
How to Compose Functions That Take Multiple Parameters: Epic Guide
James Sinclair discusses composing JavaScript functions with multiple parameters by transforming them into unary functions, using composite data structures, and employing techniques like partial application and currying for better reusability.
Lisp's Grandfather Paradox
The article explores primitive recursion's historical context, key contributors, foundational functions, limitations, and implications in programming languages, emphasizing experiential learning and philosophical connections to Lisp.
What's Functional Programming All About?
Functional programming is often misunderstood, focusing on irrelevant details. It emphasizes complexity control and clearer structures, contrasting with imperative programming's challenges, promoting better error handling and refactoring.
How to Compose Math Problems
The article highlights how composing math problems fosters creativity and critical thinking, emphasizing characteristics of "beautiful" problems and the importance of pattern recognition in problem formulation and discovery.