Stellar Supercluster is written primarily in F# (with some C#) and uses statically typed .NET bindings to the Kubernetes API, and the statically typed .NET port of the Stellar SDK.
Type-aware language support for F# is available in various .NET IDEs: VSCode, Visual Studio, JetBrains Rider, even Emacs and Vim).
F# is an ML-family language, similar in many ways to OCaml, but with smooth interoperation with the rest of the .NET ecosystem. If you have not used an ML-family language before, they take a little getting used to, but have many very desirable capabilities. Some initial orientation:
-
If you've written a functional language before (especially one with typed), the cheatsheet (in HTML or PDF) may be adequate.
-
There's a long list of further learning resources on fsharp.org, including many books and a tutorial site fsharpforfunandprofit.com.
-
A few brief points if you just want to try reading and see how it goes, here:
-
Everything is immutable by default. This is not true of the C# objects bound-to inside the .NET ecosystem or the Kubernetes objects they represent, of course; but inside F# you have to ask for mutability in order to get it, otherwise bindings (
let
variables, function arguments, fields in records, etc.) are immutable. -
Everything has a static type, and most are inferred. Everything is typed, but you don't have to write most of the types. You can however write them in many contexts -- by writing
binding:type
-- so put them in places where you want to cross-check a type, or leave one as documentation. -
Relatively lightweight and fine-grained "algebraic" type declarations are used much more ubiquitously in F# (and all MLs) than in other languages. The two main types are records (declared with
type t = {a:ty; b:ty; ...}
, fields accessable with standardx.a
dot-syntax) and discriminated unions (declared withtype t = A(ty) | B(ty) | C(ty)
). The latter are really important and different feeling than OO programming: any logic or domain of discourse that has 2 or more cases typically gives rise to a disjoint union to segregate the cases, rather than a class hierarchy; cases are then handled (separately and compiler-checked) withmatch
expressions. -
Everything is an expression, including conditionals, loops, declaration blocks. Everything nests and everything evaluates to some value. There's a trivial "unit" value denoted
()
that occurs in contexts that are a bit likevoid
or empty argument lists in C, but slightly different. If you see a function taking or being called-with()
, likefoo()
, the()
symbol there is an actual value being (logically) passed, it just as a single trivial (0-byte) inhabitant. You can bind that()
value to a variable, store it in a structure, etc. -
Function application is by juxtaposition:
foo bar
calls the functionfoo
with the argumentbar
. -
Parentheses usually surround comma-expressions, which build tuples, and many functions take tuples as arguments, so many calls look a bit like parentheses are part of calls, like
foo(bar, baz)
. Especially method calls and calls mapped into F# from other .NET languages like C#. But that is not a mandatory aspect of calling a function: it's calling a function and passing a 2-tuple. It's kinda a silly pun, but it is confusing if you are expecting parentheses to be mandatory on function calls. -
Functions can be bound, like values, using
let
. Alet
that takes a parameter is a function. For example,let foo x = x + 1
boundfoo
to the function that adds 1 to its argument. -
Other functions are attached to types and participate in .NET CLR standard receiver-based method dispatch, supporting self-polymorphism, overriding and inheritence, and so forth. These are called "methods" and are declared with
member
oroverride
blocks attached to type declarations. -
In F# the argument-tuple pun is extended to support keyword arguments, supporting calls like
foo(a=b, c=d)
. These only work on method definitions, not let-bound functions.
-