I am pleased to announce Morel release 0.6.0, just two months after release 0.5.0.

It’s been a busy couple of months. I attended Data Council 2025, gave a talk called “More than Query,” and had discussions with a lot of smart people. In a follow-up chat, Julien Le Dem and I went deeper into the topics I raised at Data Council. A piece about how Morel could do DML and data engineering generated a lot of discussion. And behind the scenes I’ve been writing a lot of code, doing fundamental work on Morel’s type system and collection types that is not yet fully baked but will appear in the next release.

Even though this is a slim release, I’m pleased to be able to add language features for logic and updating records, improvements to the shell, and performance improvements for the type-inference algorithm.

Let’s explore a few of the new features. For more information, see the official release notes.

1. Logic extensions

In logic, it is common to ask whether a predicate is true for all elements of a set, or for any elements of a set. These questions are essentially queries that return a boolean value. Morel already has ways to express those queries, but in 0.6.0 we add syntax that is closer to logic.

The new exists, forall and implies keywords have the following syntax.

exp →
  ...
| from [ scan1 , ... , scans ] step*
                                relational expression (s ≥ 0)
| exists [ scan1 , ... , scans ] step*
                                existential quantification (s ≥ 0)
| forall [ scan1 , ... , scans ] require exp
                                universal quantification (s ≥ 0)
| exp1 implies exp2

As you can see, exists and forall have similar syntax to the existing from expression. Collectively, these are called query expressions, and are all documented in the new query reference.

The new constructs are specified in terms of existing from, count, not, and orelse operators, as shown in the following table. (Morel currently uses the rewrites in the table, but that doesn’t prevent us from using an equivalent but more efficient rewrite – say using a semi-join rather than count – in future.)

Original exists
exists scan1 , ... , scans steps
Rewritten using count
count (from scan1 , ... , scans steps ) > 0
Original forall
forall scan1 , ... , scans
  require exp
Rewritten using not exists
not (exists scan1 , ... , scans
  where not exp)
Original implies
exp1 implies exp2
Rewritten using not and orelse
not exp1 orelse exp2

To avoid confusion, the previous Relational.exists function, which is a synonym for List.null and tests whether a list is non-empty, has been renamed Relational.nonEmpty.

Example 1. Existential quantification

Are there any employees with a salary greater than 1,000?

(* Using new "exists" keyword. *)
exists e in emps
  where e.sal > 1000.0;

(* Equivalent using "from" and "List.null". *)
not (List.null (from e in emps where e.sal > 1000.0));

(* Equivalent using "from" and "compute". *)
(from e in emps
  where e.sal > 1000.0
  compute count) > 0

The logic expression “∃ eset. predicate” maps to the Morel expression “exists e in set where predicate”.

exists has a syntax very similar to from; it starts a query pipeline, and therefore you may add steps such as join, group, and order. The expression yields true if the query returns at least one row.

Example 2. Universal quantification

Do all programmers have a salary greater than 900?

(* Using new "forall" keyword. *)
forall e in emps
  where e.job = "PROGRAMMER"
  require e.sal > 900.0;

(* Equivalent using "exists". *)
not (exists e in emps
  where e.job = "PROGRAMMER"
  andalso not (e.sal > 900.0));

(* Equivalent using "from" and "List.null". *)
List.null (from e in emps
           where e.job = "PROGRAMMER" andalso not (e.sal > 900.0));

The logic expression “∀ eset. predicate” maps to the Morel expression “forall e in set require predicate”.

Like from and exists, forall starts a pipeline, but that pipeline must end with a require step. The query evaluates to true if the predicate returns true for all rows that make it into require. (If there are no rows, the result is trivially true.)

Example 3. Implication

Why did we add require when where does almost the same thing? Our initial syntax only had where, and to solve the previous query many people would end up writing something like this:

(* A query using "where" is invalid and not equivalent to the
   original query. *)
forall e in emps
  where e.job = "PROGRAMMER" andalso e.sal > 900;

This query is invalid (we decided that a forall query must end in require, for reasons that will become clear) but even if it were valid, it would be incorrect. It is telling us whether all employees are programmers who earn more than 900. If just one employee were a manager earning 1,200, the query would return false. Not what we wanted!

So we added the require step. You can use where steps (and other relational operators you like) to narrow down to a population you wish to check (in this case, programmers), and finish with require to check each member of that population.

But it got us thinking about other ways to write the query. How would you write it if you were a logician? Probably like this:

(* Valid, and equivalent to the original query. *)
forall e in emps
  require not (e.job = "PROGRAMMER") orelse e.sal > 900;

The query considers the whole population (all employees, including programmers and managers) and crafts the predicate so that rows not in the population always pass. The new implies operator lets you do just that: a implies b evaluates to true if a is false, b otherwise.

(* Valid, equivalent to the original query, and we think
   quite nice even if you're not a logician. *)
forall e in emps
  require e.job = "PROGRAMMER" implies e.sal > 900;

The implies operator (like most operators, including orelse, andalso and arithmetic -, mod and /), is left-associative. “a implies b implies c” is equivalent to “(a implies b) implies c,” and hence to “(not (not a orelse b) orelse c.”

2. Record update

While thinking about syntax for updating tables we needed a way to change just one field of a record.

Suppose that the emps table has eight fields and emp represents one row from that table:

val emp = List.hd scott.emps;
> val emp =
>   {comm=0.0,deptno=20,empno=7369,ename="SMITH",hiredate="1980-12-16",
>    job="CLERK",mgr=7902,sal=800.0}
>   : {comm:real, deptno:int, empno:int, ename:string, hiredate:string,
>      job:string, mgr:int, sal:real}

If you want to change just the sal field, conventional record syntax requires you to copy the other seven fields:

val emp2 = {emp.comm, emp.deptno, emp.empno, emp.ename, emp.hiredate,
    emp.job, emp.mgr, sal = emp.sal * 2.0};
> val emp2 =
>   {comm=0.0,deptno=20,empno=7369,ename="SMITH",hiredate="1980-12-16",
>    job="CLERK",mgr=7902,sal=1600.0}
>   : {comm:real, deptno:int, empno:int, ename:string, hiredate:string,
>      job:string, mgr:int, sal:real}

Being a functional programming language, Morel avoids direct field mutation since mutation often leads to bugs. But creating an entirely new record doesn’t have to be so verbose.

We have borrowed OCaml’s syntax for functional update of records:

val emp3 = {emp with sal = emp.sal * 2.0};
> val emp3 =
>   {comm=0.0,deptno=20,empno=7369,ename="SMITH",hiredate="1980-12-16",
>    job="CLERK",mgr=7902,sal=1600.0}
>   : {comm:real, deptno:int, empno:int, ename:string, hiredate:string,
>      job:string, mgr:int, sal:real}

The with keyword in a record expression tells Morel to copy the original record (emp in this case) and only modify the fields specified.

3. Tabular mode

When giving demos of Morel queries and programs, people quickly understand that the programs are reading data from a database (such as the emps table in the scott database that is so ubiquitous in Morel’s examples and documentation). But it takes them a bit longer to realize that Morel is actually executing queries. Why is that?

Maybe it’s because the query results don’t look much like queries.

from d in scott.depts
    join e in scott.emps on e.deptno = d.deptno
  where e.job = "CLERK"
  yield {d.dname, e.empno};
> val it =
>   [{dname="ACCOUNTING",empno=7934},{dname="RESEARCH",empno=7369},
>    {dname="RESEARCH",empno=7876},{dname="SALES",empno=7900}]
>   : {dname:string, empno:int} list

By default, result sets are condensed into a compact representation, which works well for many of Morel’s supported data types. However, this format obscures the natural structure of tabular data. Let’s use the new tabular mode:

set ("output", "tabular");
> val it = () : unit

Now the tabular structure is clear:

from d in scott.depts
    join e in scott.emps on e.deptno = d.deptno
  where e.job = "CLERK"
  yield {d.dname, e.empno};
> dname      empno
> ---------- -----
> ACCOUNTING  7934
> RESEARCH    7369
> RESEARCH    7876
> SALES       7900
>
> val it : {dname:string, empno:int} list

Tabular mode only activates if the data set is a list of records; otherwise it falls back to the “classic” mode. There’s still much room for improvement – such as how values are formatted, and handling types like option, nested records, and nested lists of integers and strings – but at least your tabular data now looks tabular.

4. Unifier performance improvements

Type inference is at the heart of Morel. We believe that a well-designed, strong type system is the foundation for maintainable software, but also that programmers shouldn’t need to sprinkle types all over their program to make it compile. Therefore the language needs to be able to infer types for itself, and the gold standard is the Hindley-Milner type inference algorithm.

That algorithm needs continuous improvement. For the upcoming ordered and unordered multisets feature we need overloaded operators, and so we are extending the type system.

This release, we have tuned the internal data structures of the unification algorithm that underlies type-inference. Expect further evolution in future releases.

Conclusion

Two months since the previous release, and with luck another release of Morel in a few weeks. Work continues on the features for that release; we are especially looking forward to launching multisets, when they are ready.

Until then, give Morel a try. Go to GitHub and you can have Morel built and running in under a minute.

If you have comments, please reply on Bluesky @julianhyde.bsky.social or Twitter: