• Stars
    star
    303
  • Rank 137,655 (Top 3 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created about 2 years ago
  • Updated 7 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Foi: a different kind of functional programming language

Foi: a different kind of functional programming language


ONLINE FOI PLAYGROUND


Foi is a programming language that pragmatically balances Functional Programming (FP) and imperative programming techniques. It pulls inspiration for various syntax and behaviors from a variety of languages, including: JS, Scala, Haskell, F#, Go, Clojure, Scheme, and others.

greetings("my friend");
// Hello, my friend!

defn greetings(who) {
    def msg: `"Hello, `who`!";
    log(msg)
}

The language is designed for general application programming purposes, but with a clear emphasis on FP (and de-emphasis on OOP). It's not trying to compete in performance or capability with systems languages like C or Rust. Eventually, Foi will compile to WASM so it should be usable for applications in a variety of environments, from the web to the server to mobile devices.

An important design goal is that a somewhat experienced developer -- especially one with both FP and imperative experience/curiosity -- should be able to sufficiently/fully learn Foi in a few days. It also shouldn't take reading thousands of pages of books or watching months of workshop videos to fully grasp the surface area of Foi.

Hopefully, without too much heavy orientation, code like this will settle into familiarity for you:

defn getFavoriteMovies(userID) ^(IO ~<< {
    def movieIDs:: fetch(`"/movies/`userID`");
    def movieTitles:: all(
        movieIDs ~map (movieID) {
            fetch(`"/movie/`movieID`")
        }
        ~map (.)|,"title"|
    );
    def itemsHTML: (~fold)(
        movieTitles,
        "",
        (html,title) { `"`html`<li>`title`</li>" }
    );
    ::document.body.setHTML(`"<ul>`itemsHTML`</ul>")
});

getFavoriteMovies(123).run()

TL;DR

If you're already convinced and ready to jump in, you may want to check these out next:

But if you're still skeptical, please read on for more about the intent and design philosophy of the Foi language.

Mission

Foi promotes coherence and safety through declarative Functional Programming (FP) patterns as first class language features, while bridging (with familiar idioms) to those more experienced in traditionally imperative programming languages.

The rightβ„’ things should be intuitive, the risky things should be apparent, and the problematic things should be difficult or impossible.

Foi is a language you write for other humans to read first. It's only a secondary benefit that the computer can understand the code and execute the desired operations.

Design Philosophy

Foi aims to be a novel mix of a variety of syntactic styles and ideas from various languages. One primary goal is for Foi features to have internal consistency with each other, built on self-evident (as much as possible!) semantics and mental models. As such, there will be parts of Foi that should look familiar and other parts that may feel quite unfamiliar at first.

The syntax and design decisions attempt to diverge enough from familiar languages to take useful steps forward, but not too much that Foi is unuseful or impractical.

defn factorial(n)
    ![n ?> 1]: 1
    ^(n*factorial(n-1));

// or (tail-recursive):

defn factorial(n,total:1)
    ![n ?> 1]: total
    ^factorial(n-1,n*total);

Programming language design is a delicate balance, and inevitably will be judged both on subjective aesthetics and on empirical outcomes. And it's impossible to design a perfect language that everyone loves at first glance. Some will appreciate Foi, others will dislike it.

To prepare for exploration of Foi, here are some aspects of the design philosophy:

  • Words vs Symbols I don't think a language should be all symbols. I struggle to memorize arbitrary symbol combinations just as much as anyone. But I similarly feel overburdened when a language is full of long lists of reserved keywords.

    I dislike how these reserved keywords can visually appear indistinct from our variables/identifiers (save for syntax highlighting). I also dislike how keywords can conflict with very useful variable names in certain contexts.

    Foi has a pretty short list of bare keywords, most of which are actually part of the type annotation system (int, bool, etc). Foi has a variety of purely symbolic operators, like + (addition, concatenation) and +> (compose-left).

    But in compromise in between, there are several "named operators", that combine both a symbol and a word. In such cases, the symbol is always the first character, and helps visually distinguish from any other identifier (either built-in or user-defined).

    Another example: ^ replaces the return keyword. This is designed to be a single "return" signifier that stands out (it's vertically top-aligned), has a semantic signal (sending a value "up and out" from a function), but is short enough to not burden even the most concise of inline function expressions.

  • Consistency Building on the last point, all boolean operators begin with the ? symbol (or ! for the negated form), so they should be easily recognizable as such. This includes pure symbolic operators, like ?= (equality) / != (inequality) or ?> (greater-than), as well as named operators, like ?and (boolean-AND) and !or (boolean-NOR) and ?in (included-in).

    The ? and ! symbols can also serve as unary prefix operators on any identifier (or value), to cast the value to a boolean (and negate it, for !). Again, these symbols always indicate boolean purposes. Consistency.

    Lastly, all decision making in Foi uses ? and !. For example, there's pattern-matching ?{ .. } / ?(..){ .. } -- a more powerful combination form of if..else if and switch. There's also guard clauses ?[ .. ]: -- like if statements in front of statements. Finally, loop conditionals use the same form as guard clauses.

    Again, all these decision-making features consistently use the ? / ! signifiers.

    Loops/comprehensions all use ~ as the first symbol, from the ~each / ~map named-comprehensions to the ~< chain/bind/flatMap operator.

    Lexical definitions come in three forms: variables, functions, and types. Accordingly, there's three corresponding keywords: def (variables), defn (functions), and deft (types).

    These kinds of choices help visual distinction (from other identifiers/syntax) but also conceptually group related features/capabilities together, and hint/signal semantics.

  • Visual Semantics Many of the operators use symbol(s) with intended visual semantics.

    For example, +> (compose left-to-right operator) has two semantic signals in it. First, the + signifies a concatenation of functions (aka "composition"). Secondly, the > symbol is pointing in the direction of data-flow through the functions. By contrast, <+ is the compose-right (right-to-left), meaning the data flows in the opposite direction.

    As mentioned previously, ^ points upward, to semantically hint "return value up/out".

  • Operators As Functions If an operator has one or two operands (e.g., !x, y + z), it can be used in traditional infix/prefix form.

    But if the operand needs three or more operands, it must be invoked as a typical function call, with a ( .. ) delimited arguments list. This can be done by surrounding the operator in its own ( ), such as (*)(2,4,6,8) (i.e., 2 * 4 * 6 * 8); there must be no whitespace between the ( ) and the operator inside it.

    In the same way, operators can be referenced as function values like any other identifiers, such as def minus: (-); minus(3,1);.

  • Immutable Objects In Foi, Records / Tuples (both use < .. > syntax) are immutable.

    That means we don't overload { .. } to mean both blocks and object literals -- a perpetual confusion in languages like JS -- nor [ .. ] to mean both property accesses and array literals -- yet more confusion spawning.

    We also get rid of any syntax/capabilities around mutation of these values. But in place, we need ergonomic facilities for deriving new immutable Records/Tuples from existing ones.

    For example, < &one, &two, 10, 20 > is a Tuple that includes the contents of one and two Tuples, along with the values 10 and 20. The & operator (known as "pick") might evoke "pointer" / "reference" semantics from other languages. That's intentional here, as the mental model should be that those one / two immutable values are being linked to the new value rather than being copied (as ... does in JS). Similar for Records.

    The & capability also allows selective picking/linking, like < &two.3 > < &one.something >.

    Computed property names in JS again overload the [ .. ] syntax, like { [someProp]: 42 }. But in Foi, we simplify this with a single % symbol: < %someProp: 42 >. This is also how Records act as Maps (non-primitive keys): < %otherObj: .. >.

    Foi treats a "Set" as just a filtered construction (removing duplicates) of a Tuple.

    All these capabilities thus work identically for Records, Maps, Tuples, and Sets. That means you really just need to learn one data structure form: < .. >.

  • Side Effects Since there are no mutable values in Foi, the most common type of (in-program) side effect (bug!) in programming is completely impossible.

    The only possible in-program side effect is re-assignment of a variable (which is actually rarely the source of bugs, despite popular claims to the contrary). Foi allows such re-assignments, unlike many languages (especially FP languages) that have forms like const to disallow re-assignment.

    However, these re-assignment side effects must be declared if they cross a function boundary (via closure). You do so in the function signature, via the :over clause:

    defn myFn(x) :over(y) {
        y := x + z;
        // ..
    }

    The compiler enforces this requirement: any lexical variable outside a function's scope (aka, "free variable") that appears as an assignment target must be added to the function's :over clause. Note: in the above snippet, z is also an outer variable, but since it's only read (not assigned to), it does not need to be declared in the :over clause.

    Moreover, Foi provides a very powerful mechanism for managing all side effects (in program and externally), called the IO monad.

    One key (external) side effect is asynchrony/concurrency. Foi additionally provides several in-language mechanisms for expressing and managing asynchrony, including Promise, PullStream / PushStream, and CSP Channel. These are all monadic constructs, which allows strong mathematical guarantees about how you compose such behaviors.

  • Errors Errors/exceptions are a complicated (and complicating!) topic in programming language design. Especially in FP, runtime exceptions are typically seen as side effects, and are thus usually de-emphasized or avoided altogether.

    Foi's position is clear and firm: there are no runtime exceptions, and thus no need for try..catch style programming.

    Runtime errors can happen, but Foi always represents these as the monadic Left value. Thus, to program effectively in Foi, you will have to at least get familiar with handling Left values (via ~fold / ~cata).

  • Idiom Opinions There are a number of (hotly contested) idioms across programming languages that various developers either favor or shun. Often, general programming languages don't take strong positions on such subjective debates. Foi however does enforce a stance on some of them.

    One key idiom is "early return", meaning that a function may conditionally return before the end of the function. In Foi, there can only be one ^ return statement, and it must be at the top level of a function's body (not inside some block or expression).

    This means, for example, there's no such thing as a return statement from inside a loop, or from inside a conditional (guarded expression or pattern match expression).

    However, Foi does recognize a valid subset of conditional "early return" use-cases. In fact, Foi insists that they should be elevated to be visible and obvious in the function declaration header, rather than buried in the middle of the function body.

    These are basically conditions that can be checked before the function has even been fully invoked (but its arguments are computed and known); they do not require any internal function state to evaluate. In other words, these are "preconditions" that, if not met, obviate the need to run the function; its result (fixed or error) can be statically determined.

    For example, a function may return an error (via Left monadic value) if the arguments passed in are invalid/insufficient for the function's expectations. Note: This is a runtime scenario, orthogonal to a static type mismatch the compiler could have discovered and failed a build. Another example is a "base condition" in a recursive function.

    Foi "preconditions" appear in the function signature to express these "early return preconditions", like this:

    defn myFn(x) ?[x ?< 0]: 0 { .. }
    
    // or:
    
    defn myFn(x) ![x ?>= 0]: 0 { .. }

    In this function signature, the x parameter is checked, and if it's less than 0, the fixed 0 value is substituted ("early returned") in place of the function being invoked.

  • Types In the Foi language, you don't annotate "container types" -- types on variables, properties, etc. Instead, you annotate "value types" -- types on values, and on expressions.

    I believe we'll get most of the benefits of "typing" with much less syntactic and mental overhead compared to traditional language static types.

    Foi employs gradual typing, meaning that type annotations (and implied typings from inferences) are validated at compile-time if possible; but if some typing assertions cannot be validated, they're left to be checked at run-time.

    The Foi compiler will be configurable, to specify if you want to leave the run-time type assertions in, or strip them. For example, you may leave the run-time type assertions in for runs of your test suite, but not for your production distribution code.

This is not an exhaustive list of design persuasions, but it should help set the right perspective for evaluating/analyzing Foi code.

Original Design Motivations

Click to expand the following list of ideals that initially inspired the design for Foi.

Inspirational/Aspirational Design Ideas for Foi

  • versioned, with no backwards-compat guarantee
  • parsed, JIT'd, compiled (probably WASM), with full-fidelity AST (preserves everything including whitespace, comments, etc)
  • semicolons and braces (both required) -- no ASI, no implicit blocks
  • lexical and block scoped, functions are first-class and have closure
  • side effects (non-local reassignments) must be explicitly declared in function signature
  • no global scope (everything is in a module scope)
  • no circular dependencies, only synchronous module initialization
  • no references or pointers
  • no class, nor prototype, nor `this`-awareness
  • functional (tail-call optimized, pattern matching, curried function definitions, composition syntax, native monads, no exception handling, etc)
  • no `const`
  • no `let` -- block-scoped declarations are explicit syntax as part of the block
  • function auto-hoisting, but no variable hoisting
  • only one empty value
  • numeric types: int, float, bigint, bigfloat
  • everything is an expression (no statements)
  • iteration/looping (for/foreach/filter/map) are syntactic expressions, but accept functions
  • all keywords and operators are functions (with optional lisp-like call syntax)
  • records/tuples (instead of objects/arrays) that are immutable and by-value primitives
  • syntax for mutable data collection (dynamically define props/indices, like objects or arrays), but in order to use/read/pass-around, must first be "frozen" into an immutable record or tuple -- somewhat like a heap-allocated typed-array that's then accessed by a "view" (a record or tuple)
  • strings are sugar for tuples of characters, and are interoperable as such
  • optional named-argument call syntax
  • asynchrony built in (syntax for future values and reactivity/streams)
  • garbage collected
  • type awareness: weakly typed (small, limited set of type coercions), with dynamic type inferencing as well as optional type annotations on values/expressions

In addition to the above, I may pull parts of a long-ago description of earlier ideas for this language (then called "FoilScript").

Exploring Foi

If you have experience or familiarity with JS, check out the Foi vs JS Cheatsheet, as well as some screenshots of Foi vs JS code snippets.

Additionally, Foi Guide is a detailed exploration of the language.

For implementers or language design enthusiasts, a formal grammar specification is in progress.

Tools

Foi is still being designed. As such, there's no official compiler/interpreter yet.

However, Foi-Toy is an experimental CLI tool for playing around with Foi code, prior to there being an official compiler.

Foi-Toy currently supports tokenizing Foi code, and syntax highlighting (via HTML/CSS) as shown in the above screenshots. Foi-Toy can also validate code against the formal language grammar.

Additionally, there's an online version of Foi-Toy to play around with tokenization, syntax highlighting, and validation.

License

License

All code and documentation are (c) 2022-2023 Kyle Simpson and released under the MIT License. A copy of the MIT License is also included.

More Repositories

1

You-Dont-Know-JS

A book series on JavaScript. @YDKJS on twitter.
178,746
star
2

Functional-Light-JS

Pragmatic, balanced FP in JavaScript. @FLJSBook on twitter.
JavaScript
16,593
star
3

LABjs

Loading And Blocking JavaScript: On-demand parallel loader for JavaScript with execution order dependencies
HTML
2,277
star
4

asynquence

Asynchronous flow control (promises, generators, observables, CSP, etc)
JavaScript
1,743
star
5

CAF

Cancelable Async Flows (CAF)
JavaScript
1,332
star
6

monio

The most powerful IO monad implementation in JS, possibly in any language!
JavaScript
1,049
star
7

TNG-Hooks

Provides React-inspired 'hooks' like useState(..) for stand-alone functions
JavaScript
1,011
star
8

native-promise-only

A polyfill for native ES6 Promises as close as possible (no extensions) to the strict spec definitions.
JavaScript
723
star
9

A-Tale-Of-Three-Lists

Comparing various async patterns for a single demo
JavaScript
651
star
10

fasy

FP iterators that are both eager and asynchronous
JavaScript
546
star
11

FPO

FP library for JavaScript. Supports named-argument style methods.
JavaScript
449
star
12

youperiod.app

YouPeriod.app -- the privacy-first period tracking app
JavaScript
442
star
13

JSON.minify

Simple minifier for JSON to remove comments and whitespace
409
star
14

TypL

The Type Linter for JS
JavaScript
374
star
15

h5ive-DEPRECATED

**DEPRECATED** A collection of thin facade APIs wrapped around HTML5 JavaScript features.
JavaScript
324
star
16

eslint-plugin-proper-arrows

ESLint rules to ensure proper arrow function definitions
JavaScript
305
star
17

grips

Simple-logic templates
JavaScript
287
star
18

moduloze

Convert CommonJS (CJS) modules to UMD and ESM formats
JavaScript
207
star
19

ES-Feature-Tests

Feature Tests for JavaScript
JavaScript
199
star
20

let-er

DEPRECATED: Transpile non-ES6 let-blocks into ES6 (or ES3)
JavaScript
192
star
21

Incomplete-JS

"The Incomplete Guide to JavaScript" (book). @IncompleteJS on twitter.
190
star
22

revocable-queue

Specialized async queue data structure, supports revocation of values
JavaScript
176
star
23

deePool

highly-efficient pool for JavaScript objects
JavaScript
118
star
24

getify.github.io

JavaScript
113
star
25

concrete-syntax-tree

Defining a standard JavaScript CST (concrete syntax tree) to complement ASTs
107
star
26

eslint-plugin-proper-ternary

ESLint rules to ensure proper usage of ternary/conditional expressions
JavaScript
96
star
27

cloud-sweeper

A casual game built for the web.
JavaScript
94
star
28

BikechainJS

JavaScript VM engine (powered by V8); server-side environment modules; server-side synchronous web app controllers
C++
82
star
29

wepuzzleit

Demo PoC game for various advanced HTML5 js APIs
JavaScript
79
star
30

workshop-periodic-table

66
star
31

elasi

EL/ASI: Encrypt Locally, Account Secure Interchange
JavaScript
62
star
32

remote-csp-channel

Remote bridge for CSP channels
JavaScript
55
star
33

ScanTree

Scan a JS file tree to build an ordered and grouped dependency listing
JavaScript
51
star
34

dwordly-game

A game where words dwindle down to the shortest possible
JavaScript
42
star
35

stable-timers

timers with less race conditions
JavaScript
38
star
36

emdash

Simple blogging with node/iojs + GitHub.
36
star
37

domio

DOM and Event helpers for Monio
JavaScript
30
star
38

eslint-plugin-no-optional-call

ESLint plugin to disallow the optional-call operator
JavaScript
30
star
39

eslint-plugin-arrow-require-this

DEPRECATED: ESLint rule to require arrow functions to reference the 'this' keyword
JavaScript
28
star
40

import-remap

Rewrite ES module import specifiers using an import-map.
JavaScript
25
star
41

gum-try-hd

Try to enforce HD (16:9) camera aspect ratio for web-video calls
JavaScript
25
star
42

Mock-DOM-Resources

A mock of (parts of) the DOM API to simulate resource preloading and loading
JavaScript
25
star
43

make-a-game

some project files for a tutorial on making a simple web game
JavaScript
25
star
44

mpAjax

framework plugin for handling multi-part Ajax responses
JavaScript
23
star
45

asyncGate.js

DEPRECATED. asynchronous gate for javascript
JavaScript
21
star
46

tic-tac-toe-workshop

Workshop files for building tic-tac-toe in JS and <canvas>
21
star
47

esre

esre: fully configurable JS code formatting
20
star
48

workshop-chess-diagonals

17
star
49

featuretests.io

JavaScript Feature Tests... as a service
JavaScript
16
star
50

FoilScript

FoilScript: a new dialect of JS that fixes the sucky parts but still looks and feels like JS
16
star
51

literalizer

Specialized heuristic lexer for JS to identify complex literals
JavaScript
16
star
52

normalize-selector

Normalize CSS selectors
JavaScript
14
star
53

DOMEventBridge

Bridge DOM events to a JS event hub (for pubsub)
JavaScript
14
star
54

pong-around-workshop

Workshop files for building a pong-variant game in JS and <canvas>
12
star
55

workshop-wordy-unscrambler

11
star
56

shortie.me

Proof-of-concept demo for server-side JavaScript driven "middle-end" architecture
JavaScript
11
star
57

workshop-knights-dialer

8
star
58

middleend-boilerplate

Boilerplate Starting Point for Middle-end Web Architecture
JavaScript
8
star
59

demo-app-weatheround

JavaScript
7
star
60

the-economy-of-keystrokes-slides

Slides code built for "The Economy of Keystrokes" talk
JavaScript
6
star
61

santa-connect-app

Santa Connect: keeping track of your kids' nice/naughty status
JavaScript
5
star
62

nyc-bug-demo

bug demo for NYC code coverage tool
JavaScript
4
star
63

unnamed

unnamed (for now). nothing to see here. ;-)
JavaScript
2
star
64

my-lifesheets

CSS
1
star
65

breakthewebforward.com

JavaScript
1
star