Hacker News new | ask | show | jobs
by rkrzr 1278 days ago
This looks incredibly ambitious:

- There are no booleans in the language! Conditionals can still succeed or fail, but failure is defined as returning zero values and success is defined as returning one or more values.

- Verse uses a so-called 'lenient' evaluation strategy which is neither strict nor lazy, but somewhere in-between ("Everything is eventually evaluated, but only when it is ready")

- an expression does not evaluate to a value (like in Haskell), but instead to a sequence of zero or more values

- tries to bring functional logic programming into the mainstream

- Verse uses an effect system for I/O instead of monads

- A type in Verse is "simply a function". E.g. int is the identity function on integers, and fails otherwise

This all looks very mind-bending to me (in a good way). Perhaps Verse will one day be as influential for PL design as Haskell has been.

8 comments

> sequence of zero or more values

You can try this type of programming at home, say in JavaScript Make any result or argument be an Array. The problem with nulls goes away because "not there" is represented simply by an empty Array (a.k.a "sequence").

And you will not get null-errors because you can write:

   newValue =   someValue.filter(...) . map(...) ;

You don't need to test whether filter() returns an empty array or not, the map() will work for empty arrays too but simply do nothing.

This is in a sense "generalized programming" because you don't deal with individual values but always with collections of them. I sometimes wonder why I don't use this pattern more often.

Because now you can't differentiate between an actual empty array or an error. That's horrible.

Or, you actually have to use nested arrays to describe that the result might be an error or an array. But now you have to deal with an array which might contain... zero errors or even multiple ones.

That's really not a great way of doing things. Instead, do it the other way around and make null treatable in the same way as arrays are. And if you do that, you end up with what languages like Haskell do.

> you can't differentiate between an actual empty array or an error

Empty array in this pattern does not represent an error. It represents the fact that no values you asked for can be found. It is like querying a database. It is not an error if your query finds no values.

The calling code that receives the empty array might decide it is an error or that it is not. And of course you can always throw an error, that can be done with the "throw" keyword.

Getting rid of using nulls by using arrays instead is not about handling errors, but about preventing errors, by changing the semantics of your functions slightly so they never need to return null.

You can just replace "error" in my post with "not there" and the meaning stays the same though.
No not really. If every result is an Array then such an array can contain an empty array and then it itself is NOT an empty array. It's all about how you specify the semantic contract of your function.

You can say a result was found and that result is an array, possibly an empty array. Or that no result was found which is indicated by returning TOP-LEVEL empty array.

Well yeah, that's why I said:

> Or, you actually have to use nested arrays to describe that the result might be an error or an array. But now you have to deal with an array which might contain... zero errors or even multiple ones.

In other words: now you have Array<Array<?>>

And now it is only convention that the outer array must only have 0 or 1 elements. But there is no guarantee and the compiler won't help you since it cannot know. That's why this is the inferior approach compared to turning it around and making it so that null can be mapped and flattened.

I had similar thoughts (I do JS/TS dev daily but I’m very interested in a lot of what this language appears to offer). But I’d caution trying this with JS for anything more than exploring the ideas presented by the pattern. The reality is that even adding a type checker like TypeScript, and as many lint rules as you can throw at a project, it’s still too easy to end up putting nullish values into your null-safe arrays.

And I similarly wondered why I don’t use this sort of pattern more often, but that wondering is what led me to add this caution. It’s a really compelling idea, but probably extremely hard to debug when it goes wrong in a language and idiomatic ecosystem designed for it to go wrong at any time.

I guess one reason is that it takes more code to return an Array when in most cases what the caller needs is always just a single value. Many such functions never return null so it is overhead to make them return an array.

But returning an array also makes your functions more general. Maybe in the future you want to modify it so that it does return arrays of length > 1. Evolving the program to that state is then easy if you didn't lock yourself into assumption that this function will never ever need to return more than a single value.

> I sometimes wonder why I don't use this pattern more often.

Because while handy, it's not universally applicable (just like everything else :-))

I (C programmer, mostly) use sequences more often than you'd expect for parameters[1], but that does not mean that it makes sense to always return sequences.

In a lot of cases, sure, a function should return a sequence of values. In many other cases, it makes no sense - `if (canProceed(x,y,z))` reads more sensibly than `if (canProceed(x, y, z)[0]`.

[1] My string functions mostly lend themselves well to unlimited number of parameters. For example concatenation takes unlimited parameters and concats all of them, which it returns. Same with substring searching - take a source string and an unlimited number of substrings to find.

I agree it often makes for more readable code to return a single value instead of an array. But it makes for more maintainable code to return an array.

Whatever you can do with a single value you can also do with an array containing just that single value. How practical that is depends on the language used. In JS after ES6 arrays are syntactically quite nice and easy to use.

I think this pattern is so useful that I've been working on a library to use it for more types of things. https://www.npmjs.com/package/schtate

This includes a state monad that allows you to apply .map just like an array and a maybe monad that let's you do the same on values that might be nullish.

Keep us posted
> This all looks very mind-bending to me (in a good way). Perhaps Verse will one day be as influential for PL design as Haskell has been.

I'm not convinced. Making non-determinism a first-class feature in the language might be good if you simply care about logical specifications that might enable you to prove something correct, but a real-world program implementation has to make heuristic choices for efficiency as to when to evaluate what (and perhaps where, especially in a parallel/distributed setting), and just stating that evaluation is "lenient" isn't really saying anything worthwhile. So this kind of design choice will ultimately be expressed with ugly hacks, as with e.g. Prolog where the 'cut' feature is used to override the default search strategy.

But it's very much not non-deterministic. It's a deterministic choice. You can give it an exact semantics.
Correct, but "non-determinism" is a misnomer used in programming language theory to refer to generators and pervasive backtracking. Prolog, Icon, jq, etc. -- all fully deterministic, but known as non-deterministic owing to the pervasive generators and backtracking.
It is not a misnomer. You can use generators and backtracking to simulate non determinism. That means the simulator plus non deterministic algorithm are deterministic but the algorithm is not.
The idea in Prolog and such is that you write "goal-seeking code" and it "magically" obtains the correct answer by brute force but without the brute force being syntactically/lexically apparent. But it's still brute force. It's a catchy name, but it's not accurate. It might be more accurate if "simulation" were part of the name.
As presented in the slides it's obvious that it's a generator-paradigm, thus like Icon etc. It's really just a way of making iterators very fundamental in the language. It's deterministic and you are assume to understand that. The "PROLOG style goal searching" is probably less relevant here.
How? The slide deck calls it a "lenient" evaluation order, and doesn't really say how to make an explicit choice of what cases should be tried first. So it seems to have all the same problems as naïve Prolog.
exact semantics are given here: https://simon.peytonjones.org/assets/pdfs/verse-conf.pdf. runtime efficiency isn't specifically addressed afaict
Icon had (has!) "failure is falsity" / "value production is truth", and pervasive generators / backtracking. In Icon a function ("procedure") either: fails, returns a value, or suspends (yields) a value.

jq is very much like this too, as it has pervasive generators and backtracking, but unlike Icon jq does not distinguish between "returning" a value vs. "suspending" (yielding) a value.

The jq way kinda means you have to have booleans, and so jq does.

I'm suspicious of the no-booleans approach. After all, one could also have no integers (the Lambda Calculus doesn't have integers, having only function values!). Boolean values are just useful to have, and being able to obtain boolean values from predicates is useful too. One could do the Icon/Verse thing of failure is false / anything is true, but provide an adapter function that produces actual boolean values for when you need them.

I was suspicious of javascript where number replaces int and double. The fail in verse is a way to replace boolean and null values. I think it is the direction of evolution.
> - There are no booleans in the language! Conditionals can still succeed or fail, but failure is defined as returning zero values and success is defined as returning one or more values.

This is similar to how Icon works: https://en.m.wikipedia.org/wiki/Icon_(programming_language)

Yes, I noticed that, too. I used Icon to process a big pile of code at Apple in the late 80s and found it pleasant to work with.
This article seems like a nice overview of the pros and cons of the Icon expression evaluation system:

https://tratt.net/laurie/research/pubs/html/tratt__experienc...

I wish I could upvote comments more than once on HN :-)
Sounds like it's not a million miles away from how Clojure treats `nil` values, either
Or the List monad.
Or Common Lisp.
> so-called 'lenient' evaluation strategy which is neither strict nor lazy, but somewhere in-between

Sounds like we are still clinging on to laziness in some regard, I wish we could get shod of it entirely. Although laziness makes doing Leetcode problems in Haskell really fun, Idris got it right when they opted for strict evaluation for predictable memory performance.

There's really not much new here. All these ideas have been used in other languages previously, but perhaps not in exactly this cocktail.

I'm decidedly in the not convinced camp.

I am not convinced because far more pressing aspects have been ignored. One million programmers means program structure is more important than the syntax used to write the algorithms.

Does the language address common pain points like modifying data structures? Does it make it easy to extend existing code even without having the ability to change it?

Surprised about the syntax. Curlies still hold a lot of power :)
> Perhaps Verse will one day be as influential for PL design as Haskell has been.

Has Haskell really been that influential though? It seems to me that MLs, Lisps, Schemes, and even Erlang have been more influential.

It's definitely the most influential of the statically typed functional languages. All other statically typed functional languages or libraries inherit a lot from Haskell implementations, type system and type class concepts.
Standard ML is more influential because it's the foundational statically typed functional language. But Haskell has gained more popularity with the general programming public, not just academics.
What modern language in use today was influenced by Haskell other than maybe Idris?

(To be clear, I am mot necessarily down on Haskell or what part it has played. It’s just that many languages seem to be stealing ideas from the languages I’ve listed more than Haskell, which was the criteria I had in mind.)

Rust's traits come to mind as one example.
As far as I can tell, from an admittedly short amount of searching and research, is that type classes in Haskell were influenced by Standard ML: https://people.csail.mit.edu/dnj/teaching/6898/papers/wadler...

> This paper presents type classes, a new approach to ad-hoc polymorphism. Type classes permit overloading of arithmetic operators such as multiplication, and generalise the "eqtype variables" of Standard ML. Type classes extend the Hindley/Milner polymorphic type system ...

And it was my understanding that traits came more from the object-oriented world, such as Self and Smalltalk. Scala and Racket also had them before Rust, and Scala also has type classes.

Phil Wadler, the first author of the paper you cite, was literally a principal designer of Haskell. SML does not have type classes; your quote points out a deficiency in SML that motivates type classes.

My understanding is that "trait" is an unfortunately overloaded term. Traits in Rust are much more closely related to Haskell's type classes than to traits in the OOP sense.

I'm pretty sure Haskell is the reason why True and False are capitalized in Python.
It's influential but the most is stretching it.

It showed that monads can be used to isolate I/O but have a very real cost when it comes to complexity, that laziness by default is a bad idea and that typeclasses are a good way to introduce ad hoc polymorphism.

Definitely a good result for a research language but far less ground-breaking than ML was (admittedly putting the bar very high).

The first language to implement type classes. The first language to structure computation with monads, which are now omnipresent in functional languages.
Haskell has been enormously influential in the study of types; modern high-level type checkers are built using knowledge from these studies.
I think it influenced python list comprehensions.