Chapter 2: The Basics

Where you will learn the fundamentals of Lux programming.


Modules

Lux programs are made of modules.

A module is a file containing Lux code, and bearing the extension .lux at the end of its name (like our main.lux file).

Modules contain a single module statement, various definitions and a few other kinds of statements as top-level code (that is, code that is not nested within other code).

Definitions

Definitions are the top-level or global values that are declared within a module. They may be of different types, such as constant values or functions, or even fancier things like types, signatures or structures (more on those in later chapters).

Also, definitions may be private to a module, or exported so other modules can refer to them. By default, all definitions are private.

Value

Values are just entities which carry some sort of information. Every value has a type, which describes its properties.

Lux supports a variety of basic and composite values:

  • Bool: true and false boolean values.
  • Nat: Unsigned integers (64-bit longs, in the JVM environment).
  • Int: Signed integers (64-bit longs, in the JVM environment).
  • Real: Signed floats (64-bit doubles, in the JVM environment).
  • Frac: Unsigned numbers in the interval [0,1) (with 64-bit precision, in the JVM environment).
  • Char: Characters.
  • Text: Strings.
  • Unit: A special value that sort-of represents an empty value or a non-value.
  • Function: A first-class function or procedure which may be invoked, or passed around like other values.
  • Tuple: An ordered group of heterogeneous values which may be handled as a single entity.
  • Variant: A value of a particular type, from a set of heterogeneous options.

Note: The Bool, Nat, Int, Real, Frac and Char values in Lux are actually represented using boxed/wrapper classes in the JVM, instead of the primitive values.

That means Bool corresponds to java.lang.Boolean, Int corresponds to java.lang.Long, Real corresponds to java.lang.Double, and Char corresponds to java.lang.Character.

The reason is that currently the infrastructure provided by the JVM, and it's innate class/object system, is better suited for working with objects than for working with primitive values, with some important features available only for objects (like collections classes, for example).

To minimize friction while inter-operating with the host platform, Lux uses the boxed/wrapper classes instead of primitives, and (un)boxing is done automatically when necessary while working with the JVM.

Types

Types are descriptions of values that the compiler uses to make sure that programs are correct and invalid operations (such as multiplying two bools) are never performed.

The thing that makes Lux types special is that they are first-class values, the same as bools and ints (albeit, a little more complex). They are data-structures, and they even have a type... named Type (I know, it's so meta). We'll talk more about that in later chapters.

Macros

Macros are special functions that get invoked at compile time, and that have access to the full state of the compiler.

The reason they run during compilation is that they can perform transformations on code, which is a very useful thing to implement various features, DSLs (domain-specific languages) and optimizations. We'll also explore macros further in later chapters.

Comments

We haven't seen any comments yet being used in Lux code, but Lux offers 2 varieties:

  1. Single-line comments:
    ## They look like this.
    ## They all start with 2 continuous # characters and go on until the end of the line.
    
  2. Multi-line comments:
    #( The look like this.
       They can span as many lines as you need them to.
       #( And they can even be nested inside one another. )# )#
    #(Psss! The space-padding I added is entirely optional; but don't tell anyone!)#
    

Expressions

An expression is code that may perform calculations in order to generate a value.

Data literals (like int, tuple or function literals) are expressions, but so are function calls, pattern-matching and other complex code which yields values.

Macro calls can also be involved if the macro in question generates code that constitutes an expression.

Statements

Statements looks similar to expressions, except that their purpose is not to produce a value, but to communicate something to the compiler. This is a bit of a fuzzy line, since some things which also communicate stuff to the compiler are actually expressions (for example, type annotations, which we'll see in next chapter).

Examples of statements are module statements and definitions of all kinds (such as program definitions).

Programs

Lux doesn't have special "main" functions/procedures/methods that you define, but the program: macro accomplishes the same thing and works similarly.

It takes a list of command-line inputs and must produce some sort of action to be performed as the program's behavior. That action must be of type (IO Unit), which just means it is a synchronous process which produces a Unit value once it is finished.

Command-Line Interface

Lux programs can have graphical user interfaces, and in the future they may run in various environments with much different means of interfacing with users, or other programs.

But as a bare minimum, the Lux standard library provides the means to implement command-line interfaces, through the functionality in the lux/cli module.

That module implements a variety of parsers for implementing rich command-line argument processing, and you should definitely take a look at it once you're ready to write your first serious Lux program.

Functional Programming

This is the main paradigm behind Lux, and there are a few concepts that stem from it which you should be familiar with:

  • Immutable Values: The idea is that once you have created a value of any type, it's frozen forever. Any changes you wish to introduce must be done by creating a new value with the differences you want. Think, for instance, of the number 5. If you have 2 variables with the same number, and you decide to change the value in one variable to 8, you wouldn't want the other variable to be affected. Well, the same idea applies to all values. This is clearly a departure from the imperative and object-oriented style of having all data be mutable, but it introduces a level of safety and reliability in functional programs that is missing in the imperative style.
  • First-Class Functions: This just means that functions are values like any other. In most languages, functions/methods/procedures are more like features you register in the compiler for later use, but that just remain static in the background until you invoke them. In functional programming, you can actually pass functions as arguments to other functions, or return them as well. You can store functions in variables and inside data-structures, and you can even produce new functions on the fly at run-time.
  • Closures: Functions that get generated at run-time can also "capture" their environment (the set of local variables within the function's reach), and become closures. This is the name for a function which "closes over" its environment, making it capable to access those values long after the function was originally created. This allows you to create functions as templates which get customized at run-time with values from their environment.

Now, let's talk a bit more about the program we saw last time.

In the previous chapter we compiled and ran a Lux program, but nothing has been explained yet. Let's review the code and see in detail what was done.

(;module: {#;doc "This will be our program's main module."}
  lux
  (lux (codata io)
       [cli #+ program:]))

(program: args
  (io (log! "Hello, world!")))

The first part of this program is the module declaration.

All Lux modules automatically import the lux module, but they don't locally import every single definition, so everything would have to be accessed by using the lux; prefix or the ; (short-cut) prefix.

To avoid that, we import the lux module in a plain way.

By the way, what I just explained about the lux module is the reason why we couldn't just use the module macro as module:.

Also, the lux module is the where log! function resides.

Then we import the lux/codata/io module, also in a plain way. What that means is that we're locally importing every single exported/public definition within that module, so we can access it easily in our own module (more on that in a moment). Also, we're not giving this module any alias, and instead we will always refer to it explicitly as lux/codata/io, should we ever want to prefix something. Notice how we express nested modules (up to arbitrary depths) by simply nesting in parentheses. The lux/codata/io module, by the way, is where we get the io macro that we use later.

Finally, we import the lux/cli module. Notice how the syntax is a bit different in this case. Here, we're saying that we don't want to locally import any definition within it, except program:. Also, we're giving the lux/cli module the shorter alias cli.

Now, let's analyse the actual code!

We're defining the entry point of our program (what in many other languages is referred to as the main function/procedure/method). We'll be receiving all the command-line arguments in a (List Text) called args, and we must produce a value of type (IO Unit).

We'll go into more detail about what IO and Unit mean in the next chapter.

Suffice it to say that the log! function will produce a value of type Unit after printing/logging our "Hello, world!" text, and the io macro will wrap that in the IO type.

That (IO Unit) value will then be run by the system at run-time, giving us the result we want.


Now that we've discussed some of the basics of what goes on inside of Lux programs, it's time for us to explore the language in a little bit more depth.

See you in the next chapter!

results matching ""

    No results matching ""