10 ms·
Despite new language fatigue, this does look interesting. A few things that stand out: 1. use of Exceptions. These get so much hate these days, but the articu
by ledauphin 6y ago
Despite new language fatigue, this does look interesting.
A few things that stand out:
1. use of Exceptions. These get so much hate these days, but the articulation here of the reasons for allowing them is better than most of the Hacker News comment threads about them. I might just link to this section of Passerine's docs the next time somebody tries to tell me how Result<> is the only thing any programming language needs.
2. The use of fibers/coroutines as built-in error boundaries. This is such a pleasant natural extension to the Erlang proposal I find myself wondering why so few of the languages I'm familiar with have taken the same approach. Presumably I need to get out more.
3. Not sure what to think about the left-associativity of function calls, nor the lack of explicit call syntax. One of the things that bothers me about Ruby is the extra work you have to go to to pass functions as arguments to other functions. Naively, lacking explicit call syntax seems to me like a premature optimization. I'd appreciate someone's perspective on these two (separate?) points.
- scythe 6y ago>2. The use of fibers/coroutines as built-in error boundaries. This is such a pleasant natural extension to the Erlang proposal I find myself wondering why so few of the languages I'm familiar with have taken the same approach. Presumably I need to get out more. This is done in Lua, although it has a divergence between `pcall()` and `coroutine.wrap()`.
- brundolf 6y ago> The use of fibers/coroutines as built-in error boundaries Isn't this roughly what JavaScript does with Promises?
- slightknack 6y ago'roughly' ;)
- sterlind 6y agomy issue with Promise-style exception boundaries is the lack of type checking or explicitness, especially since `catch(Exception)` is discouraged in C#, specifically (since e.g. you probably want to panic on null refs but handle a socket exception.) in my ideal, exceptions could only be thrown/caught at the async/await boundary because only there should you have real side effects, and there should be structure, but at that point I'm reinventing Haskell's monads.
- lifthrasiir 6y agoPeople say they hate exceptions much, but what they actually hate is either one of two subsets of exceptions: Java-esque checked exceptions and C++-style untyped (and heavy) exceptions. Result type is an implementation of much wanted middle ground: explicitly typed but ergonomic enough exceptions. Its non-abrupt nature is, in my opinion, just a side effect. By comparison exceptions, as a means to abruptly terminate the thread of computation, is not something you can simply replace (unless you do total programming). Most languages with result types do have exceptions in this sense: ML `raise`, Haskell `error`, Rust `panic!` etc. Passerine seems not much different. (Note: the whole error system is, as far as I see, completely unimplemented.)
- lopatin 6y agoLanguages that do Result + panic, or languages that do checked + unchecked exceptions. Which is best? IMO, I think "Result + panic" works better because it's a cleaner separation in the sense that handling panics is strongly discouraged. Whereas handling unchecked exceptions happens all the time, and leads to confusion. Edit: To clarify, I would consider Go to be "Result + panic" even though it doesn't have a Result interface. It has a convention of returning a tuple of "value, err", which is handled by the caller and is essentially an informal Result.
- lifthrasiir 6y ago"Panic" is a fancy name for unchecked exception. It can arise from any code, for example, by out-of-index accesses or arithmetic overflows. In the other words, if you want to allow, say, `arr[0]` without any checking, you should accept that it can abruptly terminate the current thread. (Pony is a rare example of the contrapositive.) I too agree that "Result + panic" is better for the very same reason, but it doesn't mean exceptions are gone.
- HelloNurse 6y agoHandling unchecked exceptions "happens all the time" because in certain languages and libraries, like Java, it looks like less work than tediously checking error conditions or letting the program die properly. Different kinds of software in a different language can fare very differently.
- rossjudson 6y agoThe author summarizes it nicely: "Passerine has roots in Scheme and ML-flavored languages — it's the culmination of everything I expect from a programming language, including the desire to keep everything as minimalistic yet concise as possible. At its core, Passerine is lambda-calculus with pattern-matching, structural types, fiber-based concurrency, and syntactic extension."
- slightknack 6y agoI'm glad you found the langauge interesting! I actually started Passerine after reading Bob Nystrom's blog posts on concurrency and iteration (Iteration, Inside and Out). At the time, I was also reading through the thesis on Erlang, and I was thinking how neat it would be to combine Nystrom's thoughts on concurrency with Erlang's processes. I think that I could answer 3, given I know why I did what I did ;P. So, passing a function as an argument to another function is pretty simple. If it's in a variable, say `bar`, `foo bar` will do. If it's anonymous, say an identity function like `i -> i`, you just need to group it and schloop it in: `foo (i -> i)` or `foo { i -> i }` will work. Thanks!
- omaranto 6y agoThe function call syntax is old and well-established. It is used in Ocaml and Haskell, for example, but comes from older languages that influenced them. Note that the problem in Ruby does not come from the syntax "f x" for functions applications, but rather from "f" being syntax to call f rather than just evaluating to f. This Ruby is mistake is typically not present in functional languages that use "f x" syntax for calls. Typically in those languages a function cannot have zero arguments, and for functions with no useful arguments you pass a dummy argument called "()". So "f ()", commonly abbreviated to "f()" is the syntax for calling a function of "zero" arguments in those languages.
- ledauphin 6y agothanks, this is very helpful. I think I can agree that special-casing functions that take no arguments is a reasonable approach (such things should be pretty rare), but I'm less sure I see the utility in not special-casing function call syntax in the first place. Generally, I think of function calls as "special" because they redirect active execution of the program to a different context. e.g., in: f a b the space between the letter "f" and the letter "a" means "transfer control flow to f, passing a", whereas the space between the letter "a" and the letter "b" means, essentially, "also". In a butchered English sentence, "transfer control flow to f, passing a also b". This seems far more explicit when looking at ALGOL (and following)-style f(a b) I'm curious what the argument for not special-casing general call syntax is, because I guess I've never run across a place where it was articulated!
- omaranto 6y agoWell, the "f x" notation for function calls comes from the tradition of functional programming languages, where functions calls form the bulk of the code. It makes sense to make the most common operation have the lightest syntax. Also, I suspect there is a misunderstanding in the "f a b" case. In the languages that use this syntax functions only have one argument, so that parses as "(f a) b". Both spaces means the same thing: evaluate a function; first evaluate f with argument a, which returns a function f a, then evaluate this function f a with the argument b. In these languages, what would be a two argument function in other languages, you would define instead in either of these two fashions: - As a function of a single argument which is a pair. So "f (a,b)" (which can be written as "f(a,b)" for short) calls f with a single argument, namely the pair (a,b). - As a function returning a function that uses the second argument, the "f a b" style, which means "(f a) b". These two forms are equivalent, different functional languages tend to prefer one or the other (SML, Ocaml and F# tend to use "f(a,b)", while Haskell tends to use "f a b"). Passing between one form and the other is called currying in one direction (I always forgot which one!) and uncurrying in the opposite direction.