(defn better-cond
[& pairs]
(fn [& arg]
(label result
(defn argy [f] (if (> (length arg) 0) (f ;arg) (f arg))) # naming is hard
(each [pred body] (partition 2 pairs)
(when (argy pred)
(return result (if (function? body)
(argy body) # calls body on args
body)))))))
Most Lisps have `cond` like this:
(def x 5)
(cond
((odd? x) "odd") ; note wrapping around each test-result pair
((even? x) "even"))
Clojure (and children Fennel and Janet) don't require wrapping the pairs:
(def x 5)
(cond
(odd? x) "odd"
(even? x) "even")
My combinatoresque `better-cond` doesn't require a variable at all and is simply a function call which you can `map` over etc.:
((better-cond
(fn [x] (> 3 x)) "not a number" # just showing that it can accept other structures
odd? "odd"
even? "even") 5)
Of course, it can work over multiple variables too and have cool function output:
(defn recombine # 3 train in APL or ϕ combinator
[f g h]
(fn (& x) (f (g ;x) (h ;x))))
(((better-cond
|(function? (constant ;$&))
|($ array + -)) recombine) 1 2) # |( ) is Janet's short function syntax with $ as vars
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.
Do you have any recommendations for a language where you _have to_ use these concepts. I love playing with them but I find that unless i’m paying a lot of attention in most cases I fall back to a less functional style even in a language like Janet. I’d love to find a language where you largely have to use these combinatorial logic style functions so I can’t just default back to other styles.
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!
This is interesting, but I'm not convinced it's better than the python it's being compared to. Memorizing and understanding the behavior of functions that perform control flow seems no easier than memorizing and understanding hardcoded syntax/keywords. The additional flexibility of making everything a first-class citizen allows people to write code that is too clever for its own good. I could be wrong but I think there is a broad consensus that reflection is a Bad Idea.
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?)
> I could be wrong but I think there is a broad consensus that reflection is a Bad Idea.
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.
It depends on what you want. If you want the most stabile and predictable way to specify the behavior, then static control structures have little downsides.
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.
You may not want a fresh scope for control flow as you often want to use variables from the outer scope inside the if statement. Imagine you wanted to do something like this with your if statement implemented with a function (this is how the syntax would look like using a block argument in Ruby):
state = "inactive"
if_func(condition) {
state = "active"
activate_button.disabled = true
deactivate_button.disabled = false
}
In many languages you would need to wrap `state` in something that can be passed by reference, and make the function take multiple parameters. For example in JavaScript it would turn into something like this mess:
For lambda calculus, the motto is "when everything is a function".
The boolean true is the function λx.λy.x, while false is λx.λy.y.
If b then x else y then simply becomes b x y.
In a functional language like Haskell that is basically a typed lambda calculus with lots of syntactic sugar, we can replicate this with:
type MyBool a = a -> a -> a
myTrue :: MyBool a
myTrue = \x y -> x
myFalse :: MyBool a
myFalse = \x y -> y
myIf :: MyBool a -> a -> a -> a
myIf b myThen myElse = b myThen myElse
main = print $ myIf myTrue "true" "false"
This is both the Church encoding and the Scott encoding of the abstract data type
data Bool = True | False
making it pretty much the only encoding you find in the literature.
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.
The drawback is that this approach elevates code blocks to first class. It means that there is a semantical difference between a value that is a block and a value that is a result of a block. This reduces code clarity, because now block def/result is discriminated by context instead of syntax.
- 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.
This criticism seems at face value to also apply to first-class functions, which I thought was a totally uncontroversial pattern. Do you dislike those too?
Interesting that this article makes no mention of eager vs lazy evaluation - isn’t a big reason that if, for etc has to be special forms in an eagerly evaluated language that their arguments need to be lazily evaluated, which of course, deviates from the rule?
Also, lazy evaluation is achieved in an eagerly evaluated language as simply wrapping a block of code in a function, which makes lazy evaluation isomorphic with the contents of the article
> You might wonder: “Won’t the block execute immediately when passed as an argument?” Here’s the key insight: in Rye, code blocks { ... } are values. They don’t evaluate until you explicitly tell them to.
The Trade-offs section doesn't list the biggest one.
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.
That is sort of like saying all visual art projects want to become "the million dollar / pixel homepage" so providing an empty canvas and full color palete will just enable people to create visual sewage because nothing stops them from doing so.
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 :)
By all means, go full avant-garde. I have nothing against experimental stuff like this. The "throw it at the wall an see what sticks" idea.
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.
That analogy may not be suitable for this case because value proposition between the aesthetics vs the function is different for visual art projects compared to software. There is also the maintainability factor where most aged software (especially the closed source ones in private sector) change maintainers every few years. Old maintainers most often lose access to the source code and become unreachable after leaving their job.
Yes, a lot of REBOL ideas, that Rye took, or various functional, homoiconic langauges implement (haskell, lips, clojure, scheme, Io, Factor) come from search for greater internal consistency than your ALGOL-derived status-quo languages provide. :)
So degrading the core doesn't make much sense if you are not more specific. This is the core.
This discussion makes me so happy because people still care about programming languages and not just on stupid Java or whatever is making gobs of money. LISP should have a much larger following than it does, though I fully admit it has its own warts.
I agree. This is not something you would usually use to program, at least not all three together, but as it's written it's consistent and signals exactly what it does, to someone that knows Rye conventions / rules.
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`
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.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!
Dead Comment
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.
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.
E.g.
If you want to do clever stuff. I never feel the need as I would rather abstract over bigger things.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.
- 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
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 :)
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.
> 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