Morel release 0.6.0
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 “∃ e ∈ set. 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 “∀ e ∈ set. 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:
I'm please to announce release 0.6 of @morel_lang, just 2 months after 0.5. The release adds 'forall', 'exists' and 'implies' keywords for logic programming, 'with' for easy record updates, and a tabular output mode to the shell. https://t.co/XDlFQLcv4K
— Julian Hyde (@julianhyde) May 3, 2025