When If Is Just a Function
Posted3 months agoActive2 months ago
ryelang.orgTechstoryHigh profile
calmmixed
Debate
70/100
Programming LanguagesFunctional ProgrammingControl Flow
Key topics
Programming Languages
Functional Programming
Control Flow
The article discusses implementing 'if' as a function in a programming language, sparking a discussion on the trade-offs and implications of such a design choice.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
3d
Peak period
79
Day 4
Avg / period
26
Comment distribution104 data points
Loading chart...
Based on 104 loaded comments
Key moments
- 01Story posted
Oct 14, 2025 at 10:20 AM EDT
3 months ago
Step 01 - 02First comment
Oct 17, 2025 at 7:52 PM EDT
3d after posting
Step 02 - 03Peak activity
79 comments in Day 4
Hottest window of the conversation
Step 03 - 04Latest activity
Oct 29, 2025 at 4:39 PM EDT
2 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45580347Type: storyLast synced: 11/20/2025, 4:32:26 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
TXR Lisp: (relevant to this article) there is an iff function that takes functional arguments.
Square the odd values in 0 to 9:
The use function is a synonym of identity: i.e. just use the incoming value as-isSquare the even ones instead by inverting oddp with notf:
Get rid of use with iffi: a two-argument iff with an implicit identity else: Now about the point about Lisps and if: the regular if operator with value and expression arguments has a companion if function: Unlike in some other dialects like Common Lisp, a symbol can have a binding in the macro or operator space, and in the function space at the same time.But this if is not useful control. It's useful for things like being able to map over a function-like facsimile of the if operator. E.g. take an element of the (a b c d) or (x y z w) list depending on whether the leftmost list has a nil or t:
In the reverse direction, being able to write a macro for a function function exists, allows for ordinary macros to be "compiler macros" in the Common Lisp sense: provide optimizations for certain invocations of a function.This dialect is not even "weird"; overall it is Common-Lisp-like. Right down to the multiple namespaces, which is why the [...] syntax exists for referring to functions and variables in a combined virtual namespace.
> TXR is an original notation for matching entire text documents or streams, inspired by the unification that underlies logic programming systems
This has me hooked.
Lisp-2 also has irksome disadvantages, like the verbosity in code working with functional arguments.
I want to give myself and my users the advantages of Lisp-1 and Lisp-2, as well as ways to avoid their respective disadvantages, so there is no way to get around having some kind of combination that lets us work in different styles.
Looking at their rather confusing looping mechanisms, they probably could benefit from being a little more tcl-y, since tcl has some of the best looping semantics I've worked with.
I can tell the rye devs like the idea of everything being a function. But in their very first "language basics" section, they introduce assignment not as a function call, but as some kind of magic that happens when you have colons in a name.
So when we get to the "looping" section, it is the first time we have seen a colon-having-name outside the context of assignment:
> loop 3 { ::i , prns i }
And it is explained that the above line of code is "injecting" values for the code block to "pick up".
But right away this begs a number of questions:
* Why the double-colon? I would assume each loop-body-evaluation happens in its own scope, and that we're not creating global variables, so a single colon (:i) should be sufficient, right?
* What are we doing, conceptually? Is the ::i meant to be "a function which when given a value modifies its enclosing scope to include the symbol i" or "an unassigned symbol which the loop function will use to do something akin to term-rewriting with?"
* Do we really need a symbol at all, or could we just have a the point-free loop "loop 3 {prns}"?
* If we can't have the point free thing, is it because somehow the injected value would end up "to the left" of prns, if so, why would we want that?
* If we're doing something more like term rewriting, why isn't the symbol given as a separate argument from the body?
`word` - regular word, can evaluate to value it's bound to or call a funtion if bound to a function
`word: "value"` - set-word, assignment that you noticed
`probe :word` - get-word, always returns bound value, doesn't call a function if it's bound to a function, in Rye this is `?word`, because `:word` is left-set-word.
`'word` - literal word, evaluates to a word itself
etc ...
Rye adds even more word types. Rye also has left to right flow so it adds left-set-word. In Rye all assigned words with set-words are constants and they are used by default. So we also need a "mod-word", that is the double colon that you noticed, and left-mod-word. Because of left-to-right flow Rye also has .op-words and |pipe-words.
The logic around words, op-words and pipe-words ... I tried to explain here:
https://ryelang.org/meet_rye/specifics/opwords/
Another pattern you noticed (good observation btw:) is the idea of injected blocks that isn't used just for loops, but also for conditionals, function bodies, HOF-like functions etc ...
https://ryelang.org/meet_rye/specifics/injected_blocks/
All in all it is supposed to be a compact set of ideas that fit together. Some are somewhat unusual.
So I would assume that the :i is actually constant within the loop body scope. That is, the loop function is doing something like this:
; i is not assigned in this scope
evaluate {1 :i, prns i}
evaluate {2 :i, prns i}
evaluate {3 :i, prns i}
; i is still not assigned in this scope
But it sounds like you're telling me that :i would actually escape the scope of the loop body and so it needs to be modifiable or else the loop will break.
It would be costly to have this on by default. If you want separation there are many ways to achieve it. Rye has many functions related to contexts / scopes. For creating contexts in multiple ways and evaluating code inside contexts or with context as parent or isolated context, etc.
And a lot of builtins directly accept anonymous functions in place of blocks of code.
For example for loop also accepts function if you want separation and don't mind the cost.
Lisp originally, as in LISP, had assignment as a function: it was called SET.
To use it, you usually had to quote: (SET 'VAR 42).
It worked without an environment parameter because variables were in a pervasive environment, but the quote was needed to get the variable symbol as a run-time value. (SET VAR 42) would mean evaluate VAR to a symbol, and then pass that symbol to the SET function along with 42, so whatever variable was in VAR would be assigned.
Assignment is inherently non-functional, since it is a side-effect, so it is mostly counterproductive to model it as a function.
A pattern matching or logical language can have implicit bindings as the results of an operation, and so produce variables that way. Then instead of assignment you have shadowing, in that some construct binds a variable again that was already used, so that the most recent value then emerges, shadowing the previous one.
REBOL (and by extension, Rye) was never designed around the idea that everything must be a function. It just turns out that this approach fits naturally within the core principles and rules of the language.
All “active” words happen to be functions, because nothing else is needed. The behavior of different word types (and, more broadly, value types) is determined by the evaluator. In that sense, you could say that Rye does have syntax, expressed through its distinct word types.
https://code.jsoftware.com/wiki/Essays/Tacit_Expressions
https://mlochbaum.github.io/BQN/doc/tacit.html and https://mlochbaum.github.io/BQN/doc/control.html
Forth, Factor and Uiua (which combines the above approach) don't use these concepts yet are also inherently point-free, and without lambdas so you definitely wouldn't be able to rely on functional techniques!
I only know about: https://github.com/Engelberg/better-cond in clojure which is different it adds syntax enhancement + control flow convenience.
Similar better-cond can be written in clojure too:
But it's not composable as Janet's version, it will fail when mapped over, because it may return a plain value instead of a callable one. In Janet, all values can naturally participate in higher-order contexts due to its uniform treatment of callables, while in Clojure, only actual functions can be composed or mapped.- closures get tricky, i.e. having outer scoped variables within a block
- inter-block operators still need special care, e.g. return should return from a function or a nearest block, same for break/continue/etc.
[1]: https://github.com/ziglang/zig/issues/1048
Open to being convinced otherwise
(tangent but related, aren't the "Loops" and "Iteration" examples given for python literally the exact same syntax, with the exception of changing how the iterable is generated?)
Reflection may be bad in practice for other reasons/conditions, but the lack of simple/minimal/regular primitive conventions in many languages, makes reflection a basket of baddies.
The code blocks of Rye seem comparable to closures, which is a sensible thing to have. Once all code blocks are closures, there are fewer concepts to wrangle, and functional control makes excellent sense.
E.g.
If you want to do clever stuff. I never feel the need as I would rather abstract over bigger things.If you want to explore with how you can specify behaviors or rules and create new options or the ones tightly fitting your problem domain or mental model, then this gives you more tools to do so.
For example file-path, url and email address are distinct types in REBOL where in mosta languages are just strings.
=if(condition, value-if-true, value-if-false)
https://iolanguage.org/guide/guide.html#Introduction
https://www.youtube.com/watch?v=0yKf8TrLUOw
In Kernel[1] for example, where operatives are an improved fexpr.
$vau is similar to $lambda, except it doesn't implicitly evaluate its operands, and it implicitly receives it's caller's dynamic environment as a first class value which gets bound to env.$lambda is not actually a builtin in Kernel, but wrap is, which constructs an applicative by wrapping an operative.
All functions have an underlying operative which can be extracted with unwrap.[1]:https://ftp.cs.wpi.edu/pub/techreports/pdf/05-07.pdf
(This may be untrue in the presence of the likes of unsafeCoerce.)
But there are other encodings
This is quite different from the case of the natural numbers, where not only do the Church and Scott encoding differ, but there are several other reasonable representations fitting particular purposes.
Python already has conditional expressions, which already allow 'x if (predicate) else y'. Therefore in Python if is already equivalent to a function, and is composable.
Once you realize this, and also understand that Python has logical operators that can short-circuit, all Python examples feel convoluted and required the blogger to go way out of it's way to write nonidiomatic Python. If the goal was to make a point with Python, why not write Python?
I wasn't trying to make Python look awkward. I was trying to write equivalent Python, you are very much welcome to suggest improvements to my Python examples.
Other languages could be used instead of Python, for example Javascript. But I feel Python is more contained language, with clear ways to do thing (I guess I don't know them that well :) )
I don't think you understood the point I made.
My point is that Python supports conditional expressions for years.
Support for conditional expressions already means that in Python indeed "if is just a function".
Therefore, the whole premise of the article is null and void, and the article is thus pointless.
This is evident to anyone who is familiar with Python. If you have any experience with Python and you are familiar with idiomatic Python and basic features such as list comprehension, you are already widely aware how "if is just a function".
> I wasn't trying to make Python look awkward. I was trying to write equivalent Python (...)
Except you didn't. You failed to even acknowledge that Python already supports "if is just a function" with basic aspects such as list comprehension.
This basic aspect of Python is covered quite prominently in intros to Python, but somehow you failed to even acknowledge it exists, and proceeded to base your argument on a patently false claim.
If `if` is a function why don't you call it like other functions in Python?
Functions are first class in Python now AFAIK. Can you assign `if` to a variable?Also, blog-posts is not just about `if`, is `for` a function in Python, `def`, `return`, `class`?
The whole point is that yes, you can call it like a function, because Python supports it as an expression.
I seriously recommend you take the time to go through a Python tutoria.
> Also, blog-posts is not just about `if`, is `for` a function in Python, `def`, `return`, `class`?
Python supports list comprehension, so yes it is also about `for`.
Once you discover that Python supports conditional expressions and list comprehension, do you honestly believe the blog post has any merit? The blog post even shows a failure to do basic homework to verify what Python actually supports and not supports.
export gcc
Blocks are not evaluated by default, but they are eagerly evaluated if the function that accepts it decides to do so (if, do, loop) ... I understand lazy evaluation more like something that is meant to be evaluated, but physically only gets evaluated when or if you do need the result, which I'm not sure is entirely the same.
On the other hand, I would like to explore "when arithmetics is just a function". I think Elm does this well: operators are just functions with two arguments that can be written as "1 + 2", the familiar way, or "(+) 1 2". Then you can compose it like "map ((+) 2)" (currying) so you get a function that adds 2 to every item of a list, and so on.
Secretly, all code wants to be spaghetti. You and your team have to put a conscious effort into prevent that from happening. Degrading the core of the language like this is like inoculating your homebrew with sewage and expecting it not to go wrong.
I never programmed in a team, so my experience of programming is probably very different from yours. You probably want something like electric cattle fencing (if I borrow your juicy language) for your team, but if I program for my self I just want an open field of Rye I can explore :)
But what exactly does this mean in relation that what are special forms in other languages are function calls in Rye?
Is the problem that you somebody could make their own control-structure-like functions? All these function calls have exactly the same evaluation rules, which is not something you can say about "special forms" in "normal" languages because special forms are exactly rules that break the regular evaluation rules.
Rye already has a lot of control-structure-like functions in it's standard library and many other functions that accept blocks of code and aren't related to control-structures. Yes, a "stupid" person can write "stupid" code in Rye, but you don't need much flexibility in any language to write stupid code.
I fully admit there are languages that are more suited for teams, and languages that are more suited for solo developers, for this reason.
That was also my point mentioning "million pixel website" because that "art" is directionless and crowdsourced and a painter can use the same pixels or even more flexible options to create beautiful images.
According to my own experience, it's entirely possible to write a rancid spaghetti carbonara all by yourself. I'm not saying you shouldn't do it (it's a heck of a learning experience) or it should be banned or prevented or anything. But if the language comes with a tin of e. coli, at least list the side effects.
I mean this is not the first dynamic, homoiconic, reflective language. Which one of these os something I didn't list is the obvious negative or e. coli as you call it?
> experimental
These ideas have been tried and tested for 60 years now and result in less spaghetti.
So degrading the core doesn't make much sense if you are not more specific. This is the core.
I can break it down for you, but yes ... it's quite specific, I was trying to apply an `if` which is not something I needed to do or would look to do so far. The point is that you can also apply all "control structure like" functions, like any other function - consistency, not that this is advised or often used.
?word is a get word. `x: inc 10` evaluates inc function, so x is 11, but `x: ?inc` returns the inc function, so x is the builtin function.
apply is a function that applies a function to a block of arguments. It's usefull when you want to be creative, but it's not really used in run of the mill code.
.apply (op-word of apply) takes first argument from the left. `?print .apply [ "Hello" ]`
Here we needed to take second argument from the left and this is what a * modifier at the end of the op- or pipe-word does. `[ "hello" ] .apply* ?print`
You asked for it :P
Consider this example
Critical parts of the ergonomics are thata) in each branch, we have everything in scope that comes from <beginning of the function>
b) in <rest of the function>, we have everything in scope that was assigned or reassigned in the executed branch
I'd love a language that supports programmable stuff like if, since I'm tired of python autodiff not handling `if` and `for`. But it would really need programmable scope stuff to still allow the ergonomic "scope effects" that make `if` and `for` blocks ergonomic.
Rye has first class contexts and it's one of more exciting things to me about it, but I'm not sure it's related to what you describe above. More on it here:
https://ryelang.org/meet_rye/specifics/context/
One thing that pops out at the example above is that I wouldn't want to define a y inside if block, because after the if you have y defined or not defined at all, at least to my understanding of your code.
Rye is constant by default, most things don't change and you need less veriables because it's expression based and it has left to right "flow" option. So you only make variables where you really need them, which is less common and specific, it should be obvious why you need a variable.
The example I gave had a few pieces:
- x is defined prior to the if/else, and overwritten in just one branch - y is defined in both branches
So in the rest of the function, we have both x and y available, regardless of which branch is taken.
I just took a quick read of the context page and the context basics page, but it's still unclear to me whether you can program how scopes/contexts interact in rye.
In my example, we I'd say we have a few different scopes worth mentioning, and I'm curious how programmably we can make them interact in rye:
Scope 1. Right below the first x = ...: we have names available form <beginning of the function> and have x available as the ... stuff. Presumably the `foo` in `if foo` lives in this scope.
Scope 2T. Right after the true branch's y, we have scope 1 plus y introduced
Scope 2F. Right after the false branch's x and y, we have scope 1 plus x "pointing to" something new and y introduced.
Scope 3. Below the if/else, where <rest of the function> lives. This is either scope 2T or scope 2F. x is either scope 1's x or scope 2F's x, and y is either scope 2T's y or 2F's y.
In the original articles language,
So the scope relationships in an if/else are a diamond DAG taking the names from before it, making them available in each branch's scopes, and then making a sorta disjoint union of the branch's names available afterwards. Could that be programmed in rye, to allow the kinds of naming ergonomics in my previous example, but with the if/else being programmable in the sense of the original article? I'm especially interested in whether we could overload it in the traditional autodiff sense.
Responding to a different part of your comment about using names rarely in rye, I've found that I benefit a ton from handing out names more than most people do in functional languages, just for clarity and more self-documenting code. Like, the ide can say "`apples` is broken" instead of "error in location xyz" and I can rebuild my mental state better too when revisiting code.
Can you also put code into { 6 }, like { print(6) }?
Does this overwrite R's if or how does the scoping in R work?
Typically, you'd want to parse the unevaluated code, though:
It overwrites `if`, in the current scope, of course.For `List` this becomes:
The eliminator for `Nil` can be simplified to `r` as `() -> r` is isomorphic to `r`: For `Bool`: We get: Which is precisely an If statement as a function!:D