Home - IS4Code/Sona GitHub Wiki

Sona is a programming/scripting language heavily inspired by Lua, targeted for .NET. It is not compiled directly to bytecode, rather it uses F# as a transpilation target, allowing to utilize F#'s features and interop with F# assemblies. As a consequence, it contains constructs from both Lua and F#, and sometimes other .NET languages, offering high range of expressivity while being relatively easy to understand.

Background

The language can be perceived as taking the simplistic approach of Lua and adapting it to the capabilities of F#. This brings both restrictions and additional features to the language.

Why (not) Lua?

Lua is a lightweight language, offering basic but expressive syntax that focuses on clarity while allowing for extensibility and quick prototyping. It is commonly used in embedded systems due to its small memory footprint and lack of dependencies aside from ANSI C. Thanks to its popularity, it is expected to be understandable by programmers working with embedded systems and other scripting environments. Lua does not impose any single way to do object-oriented programming, rather its dynamic type system allows programmers to implement any such paradigm themselves, thanks to tables and duck-typing.

However, Lua's major advantages are also its flaws ‒ lack of static type checking means larger codebases cannot efficiently track changes without the use of static analysis. If this is a requirement, such projects generally switch to TypeScript or a similar language. Additionally, Lua's runtime does not offer features expected to be found in modern programming languages, such as better Unicode support, regular expressions, networking, or graphics, necessitating people to depend on possibly non-portable extensions.

Why (not) F#?

F#, despite being a .NET language, shares more features with scripting languages than other .NET languages ‒ its type system is static but heavily focused on inference, to the point one often does not need to specify the type of anything when writing code. Features known from C# as generics, type inference, or target-typing are merged into a single mechanism where the types of all program elements start fully blank and are subsequently refined and constrained as they are used. While a construct like var x = default; is impossible in C#, the equivalent binding in F# works out of the box, as long as the type of x is indicated by other operations. Thanks to these features, F#, in many aspects, feels like a dynamically typed language, despite maintaining all the type safety known from statically typed languages.

While there is a significant overlap between the use cases of Lua and F#, the issue is that the latter is generally utterly incomprehensible to Lua programmers:

  • The syntax is concise but people unfamiliar with it have a hard time recognizing a variable from a function, since both are defined with let. The fact that both fun and function exist in the language and serve completely different purposes also does not help.
  • Immutability by default is useful but runs contrary to expectations in many embedded systems with frequent mutations. While F# is not a pure functional language and supports some imperative constructs, the syntax is often-times more complicated than that of its "pure" counterpart.
  • Everything is an expression in F# ‒ with the exception of computation expressions, statements are just an illusion. The lack of standard "jumps" (return, break, continue) means adapting imperative code to F# is sometimes not just unidiomatic, but simply impossible to express without turning the code into an unreadable mess.

Details

Differences from Lua and F#

While Sona closely resembles Lua, there are a couple of significant differences stemming from the necessity to adapt Lua's features to make them viable for .NET, not just as a runtime but the whole ecosystem:

  • Lua does not distinguish between arrays and maps ‒ both are represented using tables, capable of being indexed with a value of any type. This is not viable in the .NET world ‒ the array is the most basic collection type, and any other collection is built around it. The syntax must therefore support creating plain arrays and distinguish sequence collections from associative collections. Likewise, arrays should be indexed from 0, despite .NET technically supporting other bounds.
  • Strings in Lua are encoding-agnostic. .NET makes having Unicode strings by default a necessity.
  • Lua has two environments where variables may be assigned ‒ global and local. The global environment is essentially just a table with string keys corresponding to various "namespaces" or variables. This dynamic environment is not possible nor desirable to mimic in .NET ‒ efficient code needs to statically bind every identifier to a known element, thus every element needs to be pre-declared. This essentially makes every environment local.
  • Member access in Lua is performed through . or : ‒ a syntactic sugar for object-oriented member access. There is a relatively low utility to being able to call a member function on another target (e.g. ("x").len("abc")), and the general impossibility of tracking whether an identifier refers to a type or an object makes distinguishing between . and : not a viable approach. The : syntax is instead reused for something Lua does natively but .NET cannot ‒ dynamic member access.
  • Lua's runtime supports both asynchronous programming and iterators through the use of coroutines, a runtime-only feature for cooperative multitasking. There is no platform-independent alternative in .NET, hence such capabilities must be expressed through special syntax.
  • Lua offers logical operators and, or, and not. So does Sona, but these operators have mutually incomparable precedence ‒ using them in any situation where the precedence would affect the result (such as a and b or c or not a or b) is disallowed.

Despite being transpiled to F#, Sona tries to be more than just an alternative F# syntax, with a couple of significant differences in addition to the syntactical relation to a different language:

  • F#'s constructs push the programmer towards immutability, e.g. let mutable is longer than let. In Sona, both approaches are equally viable, offering both var and let to one's preference.
  • F# functions come in two styles: tupled form (f(x, y, z)) and curried form (f x y z), the latter being the recommended form for F# functions (since it is just passing arguments to functions one by one). Unlike F#, currying does not happen automatically in Sona, as normal function parameters/arguments correspond to the tupled syntax in F#, since the parentheses are mandatory for multiple arguments and normal .NET methods are imported this way anyway. This would require calling curried functions like f(x)(y)(z), but a new syntax is introduced: f(x; y; z) which makes calling F# functions neater. Currying is still possible with an explicit syntax using the & operator.
  • There is little to no operator overloading in F#, instead using custom operators when necessary. Sona keeps the number of operators relatively small, merging F#'s ^, @, and .. into .., or |> and ||| into |.
  • Sona provides return, break, and continue, not present in F# (there is return but it behaves differently).

Additional differences are summarized in the table below:

General properties
Feature Lua Sona F#
Primary paradigm imperative imperative functional
Typing dynamic static (inferred) static (inferred)
Runtime Lua .NET .NET
Capabilities
Feature Lua Sona F#
Asynchronous programming through coroutines with async async computation expression
Metaprogramming metatables through F# type providers
Environment global and local local local
Syntax
Feature Lua Sona F#
Whitespace insignificant insignificant significant
Semicolons optional statement terminators optional statement terminators alternative expression separators (instead of newline)
Line comments -- // //
Block comments --[[…]] /*…*/ (*…*)
Comment nesting --[=[…--[[…]]…]=] not possible (*…(*…*)…*)
Arrays as tables: {a, b, c} [a, b, c] [| a; b; c |]
Sequences as iterators:
coroutine.wrap(function()
  coroutine.yield(a)
  coroutine.yield(b)
  coroutine.yield(c)
end)
{a, b, c} seq { a; b; c }
Maps as tables: {[x] = a, [y] = b, [z] = c} constructed from {[x] = a, [y] = b, [z] = c} constructed from seq { (x, a); (y, b); (z, c) }
Records as tables: {x = a, y = b, z = c} {x = a, y = b, z = c} { x = a; y = b; z = c }
Anonymous records as tables: {x = a, y = b, z = c} { as new|class|struct x = a, y = b, z = c} [struct]{| x = a; y = b; z = c |}
Inequality ~= != or ~= <>
String concatenation .. .. or + + or ^
C-style format %+.2f {x:+.2f} %+.2f{x}
.NET-style format not natively {x:0.##} {x:``0.##``}
Dynamic member access . (native) : ?
Mutable variable local x = y var x = y let mutable x = y
Immutable variable local x <const> = y let x = y let x = y
Literal variable not possible const x = y [<Literal>] let x = y
Raising an error message error("abc") throw "abc" failwith "abc"
Raising an exception not natively throw obj raise obj
⚠️ **GitHub.com Fallback** ⚠️