A functional programming language oriented around edge-labeled trees1.
git clone git@github.com:mkantor/please-lang.git
cd please-lang
npm install
npm run build
echo '@runtime { context => :context.program.start_time }' | ./pleaseThere are more example programs in ./examples.
This implementation of Please is a proof of concept. There are bugs and missing pieces, and language syntax/semantics may change backwards-incompatibly on the way to an official release.
Enough pieces exist to write runnable programs, but the standard library is anemic, documentation is lacking, and editor tooling is nonexistent.
The current runtime is an interpreter, but the plan is to eventually add one or more backends to allow building native executables.
A Please program is primarily composed of atoms, objects, lookups, and functions.
Atoms are the raw textual portions of your source code. They're similar to
strings from other programming languages, except there isn't a specific runtime
data representation implied by the fact that a value is an atom (e.g. the atom
2 may be an integer in memory).
Bare words not containing any reserved character sequences are atoms:
Hello
Atoms can be quoted:
"Hello, World!"
Objects are maps of key/value pairs ("properties"), where keys must be atoms:
{ greeting: "Hello, World!" }
Properties are delimited by newlines or commas; these mean the same thing:
{
a: 1
b: 2
}
{ a: 1, b: 2 }
Properties without explicitly-written keys are automatically enumerated:
{ a, b } // is the same as { 0: a, 1: b }
Data can be referenced from other places in the program using lookups, like
:en below:
{
en: "Hello, World!"
zh: "世界您好!"
hi: "हैलो वर्ल्ड!"
es: "¡Hola, Mundo!"
default: :en
}
The runtime value of the default property will be "Hello, World!".
You can index into the properties of looked-up values:
{
deeply: {
nested: {
greeting: "Hello, World!"
}
}
greeting: :deeply.nested.greeting // "Hello, World!"
}
Index keys may be computed dynamically2:
{
a: { 2: { greeting: "Hello, World!" } }
:a.(1 + 1).greeting // "Hello, World!"
}
Lookups are lexically scoped:
{
greeting: "Hello, World!"
scope: {
greeting: "Hi, Moon!"
a: :greeting // "Hi, Moon!"
}
b: :greeting // "Hello, World!"
}
Lookups can "look ahead" to properties defined later in the program:
{
b: :a // this works
a: 42
}
_ is a special name used for ignored properties/parameters and can't be
directly looked up (:_ is an error). It's legal to drill into properties named
_ via indexing, though (:a._ is fine).
Functions take exactly one parameter and their body is exactly one expression:
{
make_pair: a => { :a, :a }
}
Functions can be applied:
{
f: a => :a
greeting: :f("Hello, World!")
}
Infix notation can be used to apply binary functions (those which look like
b => a => …). For example, the expression x f y desugars to :f(y)(x).
Here's another example:
{
cons: b => a => { :a, :b }
list: 1 cons (2 cons 3) // { 1, { 2, 3 } }
}
The standard library contains symbolically-named functions for arithmetic and
other familiar binary operations. For example, 1 + 2 - 3 is 0. Also included
in the standard library are the functions|> (pipe) and >> (flow):
{
// `>>` composes functions
append_bc: :atom.append(b) >> :atom.append(c)
// `|>` pipes an argument into a function
abc: a |> :append_bc
}
All binary operations are left-associative and there is no operator precedence. Use of parentheses is encouraged.
The functions and lookups shown above are syntax sugars for keyword expressions. Most of the interesting stuff that Please does involves evaluating keyword expressions.
Under the hood, keyword expressions are modeled as objects. For example, :foo
desugars to { 0: "@lookup", 1: { key: foo } }. All such expressions have a
property named 0 referring to a value that is an @-prefixed atom (the
keyword). Most keyword expressions also require a property named 1 to pass an
argument to the expression. Keywords include @apply, @check, @function,
@hole, @if, @index, @lookup, @object, @panic, @runtime, and
@union.
In addition to the specific syntax sugars shown above, any keyword expression can be written using a generalized sugar:
@keyword { … } // desugars to `{ 0: "@keyword", 1: { … } }`
Please is a functional programming language. Currently all functions are pure, with a sole exception: logging to stderr can happen from anywhere. The specific approach to modeling other runtime side effects is still to be decided3.
Once desugared, a Please program is either an atom or an object. Please code is data in the same sense as in Lisp.
Before a Please program terminates, it prints the fully-resolved version of
itself to standard output. That means hello-world.plz can be as simple as
this:
"Hello, World!"
@runtime expressions allow accessing runtime context (like command-line
arguments). A @runtime expression is conceptually a bit like the main
function from other programming languages, except there can be any number of
@runtime expressions in a given program. Here's an example:
@runtime { context => :context.program.start_time }
Unsurprisingly, this program outputs the current time when run.
Code outside of @runtime expressions is evaluated at compile-time as much as
possible. For example, this program compiles to the literal value 2 (no
computation will occur at runtime):
1 + 1
There's no module system yet (all Please programs are single files), but that will change.
Please has a structural type system with support for subtyping, union types, generic function types, and literal (singleton) types for atoms.
Types can be inferred in most situations, but function parameters can be annotated:
{
increment: (a: :integer.type) => :a + 1
}
The ~ operator (syntax sugar for the @check keyword) can be used to annotate
the type of expressions:
:a ~ :boolean.type
An error will be raised at compile time if :a is not a boolean value.
Types are values in Please, and can be created and transformed via any mechanism
you'd use for other values (you can return them from functions, pass them as
arguments, use @if to base them on a condition, etc). For example, this works:
{
one: 1
two: :one * 2
three: @if {
:two atom.equals (:one + :one)
then: 3
else: nope
}
(3 - 2 * 3) ~ :three // this line typechecks
}
Function types can be denoted using ~> instead of => to avoid naming the
parameter, but this is merely syntax sugar. a ~> b is exactly equivalent to
(_: a) => b.
Please functions are generic, even when the parameter type is annotated. For example:
{
integer_identity: (n: :integer.type) => :n
answer: :integer_identity(42) ~ 42 // return type is `42`, not `:integer.type`
}
When you need to refer to a type parameter explicitly (e.g. to share it across
multiple expressions) introduce a "hole" with ?:
{
apply2: a =>
(f: :a ~> ?b) =>
// ^ refers to the implicit type parameter from `a =>`
(g: :b ~> ?c) =>
// ^ refers to the type parameter introduced by `?b`
:g(:f(:a))
}
To constrain a hole, write (?a: type) when introducing it. For example, these
two functions mean the same thing:
{
integer_identity_1: (n: (?n: :integer.type)) => :n
integer_identity_2: (n: :integer.type) => :n
}
Type inference follows values through the program and understands how runtime operations transform them. For example, Please knows that the natural numbers are closed under addition, so the return value of the below function is statically known to be a natural number:
(a: :natural_number.type) => (:a + 1) ~ :natural_number.type
Subtraction can underflow, so it isn't closed over the natural numbers like
addition. An analogous program using :a - 1 is rejected at compile time
because the inferred type is only :integer.type (:a could be 0, then
:a - 1 would be -1 which isn't a natural number).
Please also understands that indexing an object will yield one of the values that specific key could select:
{
scores: { alice: 1, bob: 2, charlie: 3 }
get_score: (who: alice | bob) => :scores.:who ~ (1 | 2)
}
@if expressions are similarly analyzed. When it's possible to statically
reason about the condition, Please knows which branch will be executed:
{
classify: (a: :integer.type) =>
@if {
:a < 0
then: negative
else: non_negative
}
// results are statically known to be `negative`/`non_negative`:
:classify(-5) ~ negative
:classify(3) ~ non_negative
}
The result would only be negative | non_negative if the argument's sign can't
be known until runtime.
Please is a layered language. It can be thought of as a stack of three smaller languages:
- Layer 0 (
plz) is the surface syntax. This is the language you as a human typically use to write programs. - Layer 1 (
plo) is a desugared/normalized representation of the syntax tree. - Layer 2 (
plt) is the result of applying semantic analysis, compile-time evaluation, and other reductions to theplotree. For now the language runtime is apltinterpreter, but eventually there will be compiler backends to lowerpltto machine code and/or other targets.
plz has a specific textual representation, but plo & plt could be encoded
in any format in which hierarchical key/value pairs of strings are representable
(currently only JSON is implemented, but YAML, TOML, HOCON, BSON, S-expressions,
MessagePack, CBOR, etc could be supported).
Take this example plz program:
{
language: Please
message: :atom.prepend("Welcome to ")(:language)
now: @runtime { context => :context.program.start_time }
}
It desugars to the following plo program:
{
language: Please
message: {
0: "@apply"
1: {
function: {
0: "@apply"
1: {
function: {
0: "@index"
1: {
object: {
0: "@lookup"
1: {
key: atom
}
}
query: {
0: prepend
}
}
}
argument: "Welcome to "
}
}
argument: {
0: "@lookup"
1: {
key: language
}
}
}
}
now: {
0: "@runtime"
1: {
0: {
0: "@function"
1: {
parameter: context
body: {
0: "@index"
1: {
object: {
0: "@lookup"
1: {
key: context
}
}
query: {
0: program
1: start_time
}
}
}
}
}
}
}
}
Which in turn compiles to the following plt program:
{
language: Please
message: "Welcome to Please"
now: {
0: "@runtime"
1: {
function: {
0: "@function"
1: {
parameter: context
body: {
0: "@index"
1: {
object: {
0: "@lookup"
1: {
key: context
}
}
query: {
0: program
1: start_time
}
}
}
}
}
}
}
}
Which produces the following runtime output:
{
language: Please
message: "Welcome to Please"
now: "2025-05-13T22:47:50.802Z"
}
After an eventual stable release of Please, plo & plt will be versioned to
ensure backwards compatibility.
Many compilers use intermediate representations (IRs) internally. Please's layers serve a similar purpose, though unlike some other IRs they are serializable, stable, and are designed to be human-readable (albeit verbose).
What is this good for? Use cases include:
- distributing lower-layer representations for efficiency in an eventual package manager
- experimenting with alternative syntaxes while remaining compatible with the rest of the ecosystem
- caching
plo&pltartifacts to speed up recompiles - applying new optimizations to existing programs without compiling from scratch
- manipulating
plo&pltwith any tool that can handle common formats like JSON (usejqto refactor your code!)
The first-order goal of Please is to be pleasant to use.
It strives to:
- help you express your ideas clearly, concisely, and correctly
- catch mistakes and oversights without being annoying or confusing
- be useful across many different contexts & domains
- be extensible to accommodate novel use cases
- make it easy to pay off technical debt
- emit programs that you have confidence in
The current implementation doesn't live up to these aspirations, but hopefully it approaches them over time.
Footnotes
-
Every Please program is an arborescence with edges labeled by property keys. ↩
-
Dynamic keys are checked.
{ 2: "Hello, World!" }.(1 + 41)is a type error. ↩ -
It'll probably be monads, but maybe an effect system? ↩