Readit News logoReadit News
veqq · 4 months ago
Combinatory programing offers functional control flow. (Here is a straight forward explanation: https://blog.zdsmith.com/series/combinatory-programming.html ) I was inspired to write `better-cond` in Janet:

    (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

codery · 4 months ago
This looks great, I'm still learning Janet and couldn't write it myself.

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:

  (defn better-cond [& clauses]
    (fn [& args]
      (some (fn [[pred result]]
              (when (apply pred args)
                (if (fn? result)
                  (apply result args)
                  result)))
            (partition 2 clauses))))
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.

roxolotl · 4 months ago
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.
veqq · 4 months ago
J and BQN (APL has off-ramps...)

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

hatthew · 4 months ago
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?)

Nevermark · 4 months ago
> 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.

hatthew · 4 months ago
That makes sense, thanks!
middayc · 4 months ago
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.

hshdhdhehd · 4 months ago
I agree. In any somewhat functional language (I.e. all the mainstream ones) you can wrap "if" in a function if you please.

E.g.

    function funif (b, f) {
       return (b && f())
    }

If you want to do clever stuff. I never feel the need as I would rather abstract over bigger things.

andriamanitra · 4 months ago
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:

    let state = ["inactive"];
    if_func(condition, ({state, activate_button, deactivate_button}) => {
        state[0] = "active";
        activate_button.disabled = true;
        deactivate_button.disabled = false;
    }, {state, activate_button, deactivate_button});

middayc · 4 months ago
You can do it, but that is not how the (default) control structures work in those languages. There is usually also some syntax cost.
tromp · 4 months ago
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"

nextaccountic · 4 months ago
tromp · 4 months ago
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.

Y_Y · 4 months ago

  yourIf == myId

tromp · 4 months ago

    No instance for (Eq (MyBool a0 -> a0 -> a0 -> a0))
        arising from a use of ‘==’
Undecidability of function equality aside, we could indeed define "myIf = id" instead.

ivanjermakov · 4 months ago
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.

danlitt · 4 months ago
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?
ivanjermakov · 4 months ago
First-class functions are problematic too[1], but function is always a definition. While code block is usually meant to be executed right away.

[1]: https://github.com/ziglang/zig/issues/1048

emoII · 4 months ago
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
conradludgate · 4 months ago
> 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.
emoII · 4 months ago
You're correct, that is lazy evaluation. The entire article talks about lazy evaluation without mentioning it, which was my point
foofoo12 · 4 months ago
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.

middayc · 4 months ago
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 :)

foofoo12 · 4 months ago
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.

diegoperini · 4 months ago
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.
veqq · 4 months ago
> degrading the core

> experimental

These ideas have been tried and tested for 60 years now and result in less spaghetti.

middayc · 4 months ago
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.

lisbbb · 4 months ago
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.
padjo · 4 months ago
The apply* ?thing is kinda hard to parse, would turn me off seeing lots of that in a codebase.
middayc · 4 months ago
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`

You asked for it :P