Critter is a functional-ish language that compiles to JavaScript. Critter's purpose is to explore ideas from research languages, both new and old, in the context of a modern, dynamically-typed scripting language.
Critter is pre-alpha software, and its syntax and features are still highly unstable.
@import [::ready ::find ::render ::tag] := #dom
@let hello := (name: name){
[#section [class: #main] [
[#h1 ["Hello, " name "!"]]
]]
}
@await ready()
@do find(#body.tag).render(
[hello [name: "World"]]
)
Critter has fewer types than most programming languages — only numbers, strings, records and functions. Critter also has a highly regular syntax with a handful of unambiguous forms and consistent rules for evaluation.
Critter has line comments delimited with ;
semicolon.
; this is a comment.
Critter has very loose restrictions on identifiers: any character besides whitespace and the reserved delimiters (:
;
.
@
"
()
[]
{}
) are allowed.
foo bar/baz-quux >=> 💩
Critter supports decimal and hex numbers. Decimal numbers can have negative signs, decimal points, and underscores for spacing. Hex numbers can mix upper and lowercase letters.
-123.45 1_000_00 0xDEADBEEF 0xabad1dea
Strings in Critter are delimited with "
double quotes. These can be escaped inside strings with \"
, but all other characters (e.g. line-breaks) are permitted to be used unescaped.
"foo bar baz" "\"Hello,\" he lied."
"This is a
multi-line string."
Critter also allows strings without spaces and the reserved delimiters to be written as "hashtags".
#foo #bar/baz-quux #>=> #💩
Critter has a single composite data type: the record. Records are delimited with []
square brackets. Records can have both positional (zero-indexed) and named fields.
[#foo 10.5 bar: [x: 1 y: 2]]
Record fields can be accessed directly with the ::
operator.
[#foo 2]::0 ; #foo
[x: 1 y: 2]::x ; 1
Critter function definitions use ()
round parentheses for parameters and {}
curly brackets to delimit the function body. Function parameters follow the same pattern as record fields: both positional and named arguments are permitted. The last expression in a function body is the return value.
(x y foo: z){ [z x y] }
Functions without arguments can be written without the parens:
{ #foo }
Critter supports a few different syntactic forms for calling functions, which fill the roles methods, operators and keywords have in other languages.
foo([123] bar: 45)
quux()
Instead of methods, critter allows functions to be called with .
dot syntax. Functions that take a single argument don't need trailing parens.
[123].foo(bar: 45)
[x y z].length
Additionally, critter has keyword-style syntax, delimited with the @
at-sign. Keyword syntax allows nested callback functions to be written as a series of assignments.
(user){
@try user.is-signed-in?
@let x := foo(1)
@await y := async-foo(2)
[x y]
}
; is equivalent to:
(user){
try(user.is-signed-in? {
let(foo(1) (x){
await(async-foo(2) (y){
[x y]
})
})
})
}
Critter has no notion of void
or undefined
, ie. the absence of a value. Functions always return values, even if they're evaluated for side effects. Anything that would introduce an undefined value, such as calling a function with fewer arguments than expected, or accessing a field that is not present on a record, raises an unrecoverable error.
Critter also has no notion of null
, ie. a catch-all for missing data or failure. Critter idiomatically uses result values -- records structured as [#ok value]
or [#error message]
-- to represent success or failure.
Critter has no throw
/catch
error handling. Errors are handled through error result values or by terminating the process unconditionally.
Most unusually, Critter has no true
or false
booleans. Here, Critter also uses result values. This means that all of Critter's comparison functions have control flow properties similar to short-circuit boolean operators.
(x){
@try number(x)
@returns number
+(x 10)
}
PSA: If a function accepts a string then it's a parser. Parsers are hard to get right and dangerous to get wrong. Write fewer of them.
— David R. MacIver (@DRMacIver) May 11, 2017
Most scripting languages contain a lot of features for working with strings, but few have any built-in tools for actually parsing strings into structures; they encourage you to stay in unstructured string-land instead of transforming them into structured data.
Most of Critter's peers have types for lists, sets and key-value maps, but Critter offers tables for storing and processing relational data and rules for declaratively generating data.
@let edges := [#from #to].table([
[#charles #park]
[#park #downtown]
[#downtown #south]
])
@let edges := [#from #to].rule((from to){
edges.and(edges.where([to: from from: to]))
})
@let links := [#from #to].rule((from to){
or(edges.where([::from ::to])
and(edges.where([from: from to: #between.?])
edges.where([from: #between.? to: to])))
})
links.where([from: #downtown]).rows
;[
; [from: #downtown to: #park]
; [from: #downtown to: #charles]
; [from: #downtown to: #south]
;]
Critter is a functional language, but it does not require side effects to be threaded into the program's entry point as Haskell or Elm do. Critter idiomatically uses processes to encapsulate state and nondeterminism.
@await x := future
@on message := stream
Critter has no notions of inheritance or type hierarchies, but it does allow data structures to conform to interfaces and protocols via the proto
field. Records can implement the required, implementation-specific "methods" with functions stored on the appropriate proto
field.
; protocol methods
; foo.concat(bar) => foo::proto::concat(foo bar)
@def concat := method(#concat)
@def bind := method(#bind)
; protocol static methods (no `self` argument)
; foo.unit(bar) => foo::proto::unit(bar)
@def unit := static-method(#unit)
@def empty := static-method(#empty)
@def List = [
concat: (self right){
self.match([
(#cons head tail){
cons(head tail.concat(right))
}
(#nil){ right }
])
}
bind: (self fn){
self.match([
(#cons head tail){
fn(head).concat(tail.bind(fn))
}
(#nil){ nil }
])
}
unit: (value){ cons(value nil) }
empty: { nil }
]
@def map := (self f){
self.bind((x){ self.unit(f(x)) })
}
@def filter := (self f){
self.bind((x){ f(x).cond({ self.unit(x) } { self.empty }) })
}
@def filter-map (self f){
self.bind((x){ f(x).cond((y){ self.unit(y) } { self.empty }) })
}
@def cons = (head tail){ [#cons head tail proto: List] }
@def nil = [#nil proto: List]
@module
, @import
etc@def
for mutually-recursiveCritter has a lot of ideas composed from a handful of core types. It follows the principle "It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures." This principle is the core philosophy from which the rest of the design follows.
Critter enables syntactic diversity but largely enforces functional consistency -- e.g. a function call can be written multiple ways, but functions always work the same. In a handful of cases, consistency is sacrificed for simplicity; e.g. with module-level keywords.
Critter is a small language, and eschews a lot of features and idioms that are common in other languages. It features an extensive standard library, but it is largely focused on supporting a functional programming style.
Critter is dynamically typed; there is no "undefined behavior" but it makes few guarantees beyond generating valid JavaScript. Critter makes error handling and type correctness idiomatic, but it doesn't enforce them.