Lithia is an experimental functional programming language with an implicit but strong and dynamic type system.

Lithia Programming Language

Lithia is an experimental functional programming language with an implicit but strong and dynamic type system. Lithia is designed around a few core concepts in mind all language features contribute to.

  • Composition instead inheritance
  • Predictability
  • Readability

Is Lithia for you?

No. Unless you want to play around with new language concepts for some local non-production projects with a proof of concept programming language. If so, I’d be very happy to hear your feedback!

Roadmap

Currently Lithia is just an early proof of concept. Most basic language features exist, but the current tooling and standard libraries are far from being feature complete or stable.

  • Module imports
  • Testing library
  • Easy installation
  • Prebuilt docker image
  • Prebuilt linux binaries
  • Docs generator in progress
  • Stack traces
  • Creating a custom language server
  • ... with diagnostics
  • ... with syntax highlighting
  • ... with auto completion
  • A package manager
  • Tuning performance
  • Move stdlib to a package
  • Custom plugins for external declarations
  • More static type safety

Of course, some features don't end up on the above list. Espcially improving the standard libraries and documentation is an ongoing process.

Installation

If you want to give Lithia a try, the easiest way to get started is using Homebrew. By default Lithia is now ready to go.

$ brew install vknabel/lithia/lithia

To get syntax highlighting, download and install the latest version of Syntax Highlighter with Lithia for VS Code.

Which features does Lithia provide?

Lithia is built around the belief, that a language is not only defined by its features, but also by the features it lacks, how it instead approaches these cases and by its ecosystem. And that every feature comes with its own tradeoffs. As you might expect there aren’t a lot language features to cover:

  • Data and enum types
  • First-class modules and functions
  • Currying
  • Module imports

On the other hand we explicitly opted out a pretty long list of features: mutability by default, interfaces, classes, inheritance, type extensions, methods, generics, custom operators, null, instance checks, importing all members of a module, exceptions and tuples.

Curios? Head over to the generated Standard Libraray documentation.

Functions

Lithia supports currying and lazy evaluation: functions can be called parameter by parameter. But only if all parameters have been provided and the value will actually be used, the functions itself will be called.

To reflect this behavior, functions are called braceless. Every parameter is separated by a comma.

func add { l, r => l + r }

add 1, 2 // 3

// with currying
let incr = add 1 // { r => 1 + r }
incr 2 // 3

As parameters of a function call are comma separated, you can compose single arguments. Also all operators bind stronger than parameters and function calls.

when True, print "will be printed"

// here you can see lazy evaluation in action:
// print will never be executed.
when False, print "won't be printed"

// parens required
when (incr 1) == 2, print "will be printed"

// when needed, single parameter calls can be nested
// fun (a (b (c d)
fun a b c d
// fun (a b), (c d)
fun a b, c d

Data Types

are structured data with named properties. In most other languages they are called struct.

data Person {
  name
  age
}

As data types don’t have any methods, you simply declare global functions that act on your data.

func greet { person =>
  print (strings.append "Hello ", person.name)
}

Enum Types

in Lithia are a little bit different than you might know them from other languages. Some languages define enums just as a list of constant values. Others allow associated values for each named case. Though in Lithia, an enum is an enumeration of types.

To make it easier to use for the value enumeration use case, there is a special syntax to directly declare an enum case and the associated type.

enum JuristicPerson {
  Person
  data Company {
    name
    corporateForm
  }
}

Instead of a classic switch-case statement, there is a type-expression instead. It requires you to list all types of the enum type. It returns a function which takes a valid enum type.

import strings

let nameOf = type JuristicPerson {
  Person: { person => person.name },
  Company: { company =>
    strings.concat [
      company.name, " ", company.corporateForm
    ]
  }
}

nameOf you

If you are just interested in a few cases, you can also use the Any case.

Nice to know: If the given value is not valid, your programm will crash. If you might have arbitrary values, you can add an Any case. As it matches all values, make sure it is always the last value.

Modules

are simply defined by the folder structure. Once there is a folder with Lithia files in it, you can import it. No additional configuration overhead required.

import strings

strings.join " ", []

Or alternatively, import members directly. But use this sparingly: it might lead to name collisions.

import strings {
  join
}

join " ", []

Current module

Sometimes you might want to pass the whole module as parameter, or to avoid naming collisions.

module current

let map = functor.map

doSomeStuff current

As shown, a common use case is to pass the module itself instead of multiple witnesses, if all members are also defined on the module itself.

Module resolution

Lithia will search for a folder containing source files at the following locations:

  • when executing a file, relative to it
  • when in REPL, inside the current working directory
  • at $LITHIA_LOCALS if set
  • at $LITHIA_PACKAGES if set
  • at $LITHIA_STDLIB or /usr/local/opt/lithia/stdlib

*Nice to know: there is a special module which will always be imported implicitly, called prelude. It contains types like Bool or List. As Lithia treats the prelude as any other module. Therefore you can even override and update the standard library.*

Modules and their members can be treated like any other value. Just pass them around as parameters.

Why is this feature missing?

Why no Methods?

In theory methods are just plain old functions which implicitly receive one additional parameter often called self or this.

In practice you often aren‘t able to compose methods as you can compose free functions.

Another important aspect of methods is scoping functions with their data. Here the approach is to simply create more and smaller modules. In practice we‘d create a new file for every class anyway.

data Account {
  balance
}

func withdraw { debit, account =>
  Account account.balance - debit
}
import accounts {
  Account
}

accounts.withdraw 500, Account 1000

with Account 150, pipe [
  accounts.withdraw 100,
  accounts.withdraw 50,
]

Why no Interfaces?

Interfaces only allow one single implementation per type. The only way to make the implementing types composable is to define more types, requiring more ceremony than plain old functions.

Instead of an interface you simply create a new data type, assign your implementation and pass it alongside to your argument. The instance containing the implementation is called a witness.

print greetable.greeting object } greet shortPersonGreetable, someone ">
data Greetable {
  greeting ofValue
}

let shortPersonGreetable = Greetable { person =>
  strings.append "Hi " person.name
}


func greet { greetable, object =>
  print greetable.greeting object
}

greet shortPersonGreetable, someone

The benefit of using witnesses instead of plain interface lies in flexibility as one can define multiple implementations of the protocol.

let longPersonGreetable = Greetable { person =>
  strings.prepend "Hello " person.name
}

And when it comes to composition, this approach really shines! That way we can define our own map or similar functions to transform existing witnesses.

import strings

func map { transform, witness =>
  Greetable { object =>
    transform witness.greeting object
  }
}

let uppercased = map strings.uppercased

let screamed = map strings.append "!"

As seen above, we can easily rely on existing implementations, compose them and always receive the same data types until we have built complete algorithms!

Why no class inheritance?

Classes and inheritance have their use cases and benefits, but as Lithia separates data from behavior, inheritance doesn’t serve us well anymore.

For data we have two options:

  1. Copying all members to another data. Though enums must also include this new data type.
  2. Nesting the data. Especially useful if the data is only used outside the default context. This is especially great if you need to combine many different witnesses or data types as with multi-inheritance.
data Base { value }

// copying
data CopiedBase {
  value
  other
}

// nesting
data NestedBase {
  base
  other
}

When regarding witnesses, we have a third option: modules. We create our witnesses and bind each data value directly to the module itself.

module strings

let map = functor.map
let flatMap = monad.flatMap
let reduce = foldable.reduce

doSomething strings, ""

As witnesses aren’t typically used in enums (and one could also add a Module case), we can simply import a whole module and use it as a replacement for multiple witnesses at once.

Though the defaults should be used wisely: for example the Result type has two different, but valid implementations of map! On the other hand List only has one valid implementation.

One additional feature of class inheritance is calling functionality of the super class. In Lithia the approach looks different, but in fact behaves similar: We simply create a whole new witness, which calls the initial one under the hood.

Why no dynamic type tests?

Most languages allow type casts and checks. Lithia does only support the type switch expression for enums.

These checks are unstructured and therefore tempt to be used in the wrong places. Though type checks should be used sparingly. Lithia prefers to move required decisions to the edge of the code. Witnesses should implement decisions according to the provided data and desired behavior.

Also: if there is one single type to focus on, the tooling and the developer can understand all cases much easier and faster.

License

Lithia is available under the MIT license.

Owner
Valentin Knabel
Mobile application and web developer. Also interested in dev tooling, compilers and programming languages
Valentin Knabel
Comments
  • Document how modules work

    Document how modules work

    Modules should be usable as normal values. The naming convention is being in plural and small cased.

    Modules and data can be used interchangeably (thinking of witnesses).

    Open decisions:

    • is every file or folder a module?
    • access to submodules?
  • Absolute Modules and Roots

    Absolute Modules and Roots

    More sophisticated logic around finding the correct module.

    • $LITHIA_PRELUDE as default for prelude, strings, lists, ...
    • $LITHIA_PACKAGES
    • $LITHIA_LOCALS or Dir(script)

    Where we start searching for modules from locals over packages to prelude. This allows updates and replacements of the prelude.

    We still start just one single file, not a whole module. And from now on, all imports must be absolute.

    A package manager would just clone / download all dependencies and set the $LITHIA_PACKAGES accordingly (e.g. to $CWD/.lithia).

  • LSP: modules, members, snippets, ...

    LSP: modules, members, snippets, ...

    The autocompletions and hovers should only respect current imports. Both should not rely on type inference, yet.

    These cases should be supported:

    • importable-module discovery
    • module. should autocomplete its members
    • snippets for type-switch-expressions
    • arbitrary variable . member (completions only from all data-/module-/import-/extern-members)
  • Feature: alias imports

    Feature: alias imports

    In the future there may be collisions within the last identifier of nested modules. In this case, we‘d receive a conflict.

    Sometimes we import a module, that we use very often with many members.

    In both cases it would be awesome to import a module and making it accessible using an alias.

    Though there are multiple alternatives regarding the syntax:

    • import alias = nested.module Similar to let definitions
    • import alias nested.module Similar to go and let undocumented abbreviation
    • import nested.module alias still takes the last identifier
    • import nested.module as alias like Typescript
  • LSP: module based Autocompletion and Hovers

    LSP: module based Autocompletion and Hovers

    Our language server needs correct autocompletion, that works across file bounds. Instead it should parse the whole module structure including all imports. The autocompletions and hovers should only respect current imports. Both should not rely on type inference, yet.

    These cases should be supported:

    • import members of a module, like lists.append
    • global and local declarations (func, let, enum, data, extern)
    • parameters
  • LSP: Syntax Error Diagnostics

    LSP: Syntax Error Diagnostics

    The LSP should listen to all opened and changed documents and apply centralized diffs.

    • on open/create/change it should parse the contents and report syntax errors as diagnostics
    • on delete it should delete the diagnostics
    • on closed, nothing should happen

    In background we should also import and parse all dependencies.

  • LSP: Syntax Highlighting

    LSP: Syntax Highlighting

    Using the official tree-sitter syntax highlighting would be great, but would be too much effort for now. Instead, I'd prefer to run the queries from tree-sitter-lithia and determine the highlighting manually.

  • Improved CLI

    Improved CLI

    ❯ go run ./app/lithia help
    Lithia is an experimental functional programming language with an implicit but strong and dynamic type system.
    It is designed around a few core concepts in mind all language features contribute to.
    
    Lean more at https://github.com/vknabel/lithia
    
    Usage:
      lithia [flags]
      lithia [command]
    
    Available Commands:
      completion  Generate the autocompletion script for the specified shell
      help        Help about any command
      repl        Runs interactive Lithia REPL.
      run         Runs a Lithia script
    
    Flags:
      -h, --help   help for lithia
    
    Use "lithia [command] --help" for more information about a command.
    
  • Runtime Stack Traces

    Runtime Stack Traces

    Currently only the line where the error occurs will be printed. Instead we want the whole stack trace to be printed. That would be a huge leap in debugging!

  • Refactor proper ast #18

    Refactor proper ast #18

    Separates the parsing and the execution phases as proposed by #18

    Todos regarding error reporting and handling

    • [ ] centralise error messages (later)
    • [x] replace panics with additional *RuntimeError
    • [x] improve quality of stack traces
    • [x] include information about declarations in stack traces
    • [x] report syntax errors better
    • [x] report all syntax errors
    • [x] prefer relative paths in errors
    • [x] topic distinction between runtime and type errors (later)

    What else?

    • [x] consistent print descriptions
    • [x] some doc strings are missing
    • [x] search for remaining todos
    • [x] type switch validation
    • [x] centralise function calls (and casting!)
    • [ ] can we combine Environments and Interpretation Contexts? (later)
    • [x] remove old interpreter, but not the tests
    • [x] tests seem to be executed and then printed
    • [x] squash 😅
  • Refactor: proper AST structure

    Refactor: proper AST structure

    Currently Lithia does not make a real difference between parsing and executing. In order to support any kind of advanced tooling, we need a proper AST structure.

    This should also massively reduce the performance hit of function calls as they currently get parsed again.

  • Proposal: decoupled Type-Switch for Nested Enums?

    Proposal: decoupled Type-Switch for Nested Enums?

    When defining an enum which contains another enum, all type-switches are required to address the nested enum by its name. Explicitly only matching one single case is not possible.

    This should be changed.

    enum Maybe {
      Optional
      Any
    }
    
    // currently possible
    type Maybe {
      Optional: { => },
      Any: { => }
    }
    
    // proposal
    type Maybe {
      Some: { => },
      None: { => },
      Any: { => }
    }
    
  • Proposal: Executing shell scripts

    Proposal: Executing shell scripts

    We need to run some shell scripts! But how? There are many ways, from complex observables, pipes and stuff. And we could just focus on the most basic, blocking, non-interactive implementations.

Functional programming library for Go including a lazy list implementation and some of the most usual functions.

functional A functional programming library including a lazy list implementation and some of the most usual functions. import FP "github.com/tcard/fun

May 21, 2022
[TOOL, CLI] - Filter and examine Go type structures, interfaces and their transitive dependencies and relationships. Export structural types as TypeScript value object or bare type representations.

typex Examine Go types and their transitive dependencies. Export results as TypeScript value objects (or types) declaration. Installation go get -u gi

Dec 6, 2022
Functional Programming support for golang.(Streaming API)

Funtional Api for Golang Functional Programming support for golang.(Streaming API) The package can only be used with go 1.18. Do not try in lower vers

Dec 8, 2021
Go module that provides primitive functional programming utilities.

Functional Functional provides a small set of pure functions that are common in functional programming languages, such as Reduce, Map, Filter, etc. Wi

Jun 12, 2022
Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

Advent of Code 2021 Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved

Dec 2, 2021
Jan 4, 2022
Repo Tugas Problem Solving Paradigm (Greedy, D&C, Dynamic Programming) ALTA Immersive BE5
Repo Tugas Problem Solving Paradigm (Greedy, D&C, Dynamic Programming) ALTA Immersive BE5

Cara mengerjakan tugas clone project ini, melalui git clone https://github.com/ALTA-Immersive-BE5/Problem-Solving-Paradigm.git setelah clone selesai,

Dec 23, 2021
The new home of the CUE language! Validate and define text-based and dynamic configuration

The CUE Data Constraint Language Configure, Unify, Execute CUE is an open source data constraint language which aims to simplify tasks involving defin

Dec 31, 2022
The package manager for macOS you didn’t know you missed. Simple, functional, and fast.
The package manager for macOS you didn’t know you missed. Simple, functional, and fast.

Stew The package manager for macOS you didn’t know you missed. Built with simplicity, functionality, and most importantly, speed in mind. Installation

Mar 30, 2022
A stack oriented esoteric programming language inspired by poetry and forth

paperStack A stack oriented esoteric programming language inspired by poetry and forth What is paperStack A stack oriented language An esoteric progra

Nov 14, 2021
Unit tests generator for Go programming language
Unit tests generator for Go programming language

GoUnit GoUnit is a commandline tool that generates tests stubs based on source function or method signature. There are plugins for Vim Emacs Atom Subl

Jan 1, 2023
FreeSWITCH Event Socket library for the Go programming language.

eventsocket FreeSWITCH Event Socket library for the Go programming language. It supports both inbound and outbound event socket connections, acting ei

Dec 11, 2022
Simple interface to libmagic for Go Programming Language

File Magic in Go Introduction Provides simple interface to libmagic for Go Programming Language. Table of Contents Contributing Versioning Author Copy

Dec 22, 2021
The Gorilla Programming Language
The Gorilla Programming Language

Gorilla Programming Language Gorilla is a tiny, dynamically typed, multi-engine programming language It has flexible syntax, a compiler, as well as an

Apr 16, 2022
Elastic is an Elasticsearch client for the Go programming language.

Elastic is an Elasticsearch client for the Go programming language.

Jan 9, 2023
👩🏼‍💻A simple compiled programming language
👩🏼‍💻A simple compiled programming language

The language is written in Go and the target language is C. The built-in library is written in C too

Nov 29, 2022
accessor methods generator for Go programming language

accessory accessory is an accessor generator for Go programming language. What is accessory? Accessory is a tool that generates accessor methods from

Nov 15, 2022
Http web frame with Go Programming Language

Http web frame with Go Programming Language

Oct 17, 2021
A modern programming language written in Golang.

MangoScript A modern programming language written in Golang. Here is what I want MangoScript to look like: struct Rectangle { width: number he

Nov 12, 2021