Skip to content

Latest commit

 

History

History
176 lines (115 loc) · 8.65 KB

File metadata and controls

176 lines (115 loc) · 8.65 KB

Fennel Language Crash Course—Lua

Yesterday, we already discussed Fennel's core syntax, which is sufficient for writing pure data transformations. On the other hand, a large part of real-world software development involves libraries, so we will now talk more about Lua.

Lua and Minimalism

As mentioned before, Lua is the simplest high-level language widely used in production environments. Why is that? This is because it is typically used for embedding: within a host application, Lua is usually embedded as a library. It is lightweight because it lacks built-in file systems, system calls, network connections, and other features. These low-level operations are usually handled by the host application (often in C or C++). This design philosophy makes Lua's core very lean and easier to integrate into various application environments.

C and Lua Interaction

One of Lua's major features is its seamless integration with C/C++ languages. It provides an elegant C API, allowing host applications to easily register C functions for Lua code to call. For example, if your main program needs to read a configuration file, you can write a read_config_file function in C, and then expose it to the Lua environment via the C API in the Lua library.

When Lua code calls read_config_file, control returns to the host application to execute the C function. After this function completes, the result is returned to Lua. This mechanism makes Lua not just a scripting language, but more like a highly customizable "component" that can interact deeply with the main program, used for tasks such as logic processing, configuration, or even game plugins.

Lua Overview

Data Types

Lua provides 8 data types:

  • nil: Represents "no value". If nil is used in an if condition, it is treated as false.
  • boolean: Only true and false. In Lua, only nil and false are considered false. All other values, including the number 0, the empty string "", or an empty Table {}, are considered true. This differs from the behavior of some other programming languages (like C++ or JavaScript).
  • number: Double-precision floating-point numbers.
  • string: Strings.
  • table: The only container type.
  • thread: Although the word "thread" is used, it actually refers to a coroutine.
  • function: Functions.
  • userdata: This type is used if certain types are returned by C language.

In Fennel, you can use (type $variable) to check the type of $variable.

Important Global Functions

In Lua, some functions in the standard library are not built into the global environment, so we need to require the library before we can use them. For example, io.open below:

local io = require("io")
io.open("file.txt", "r")

On the other hand, some functions are directly built into the global environment; these are global functions. Particularly important ones include:

  • tonumber: Converts string to number.
  • tostring: Converts to string.
  • print: Calls tostring on all its arguments first, then prints them followed by a newline. Spaces are inserted between arguments when printing.
  • type: Returns the type.
  • pcall: Calls a function in protected mode. Because it's protected mode, if an error occurs, the program won't crash immediately. It returns two values: the first is a boolean indicating whether the call was successful; the second is the return value or an error message.
  • error: Stops program execution, generates an error, and jumps to the nearest pcall.
  • assert: Checks a condition; if false, throws an error.
  • ipairs: Iterates over Sequential Tables.
  • pairs: Iterates over any Tables.
  • unpack: Unpacks a Sequential Table from a single Table into multiple independent values.
  • require: Loads and returns a module.

If we make a simple classification of the above functions:

  1. Helper functions: tonumber, tostring, print, type.
  2. Error and exception handling: pcall, error, assert.
  3. Container-related: ipairs, pair, unpack.
  4. Module loading: require.

Let's look at an example of unpack usage in Fennel:

(local t [:a :b :c])
(print (unpack t))	 ;; a   b   c

If you've done functional programming, does a clever function apply come to mind? Since Fennel adopts minimalism, and has already borrowed unpack from Lua, Fennel does not provide an apply function.

Standard Library

If you have any needs that the previously introduced functions cannot handle, before introducing third-party libraries, first consult the Lua 5.1 Manual. You might find what you need in Lua's standard library.

Online Compile, Decompile.

Sometimes, code generated by GenAI simply doesn't run, with even operator positions being incorrect. This is common because Fennel is a niche language, and there isn't enough training data for AI to utilize. The workaround is simple: you can first ask GenAI to generate Lua code, then use a compiler/decompiler to convert it into Fennel code.

Other Uses of Lua's Table

As previously introduced, Lua's Table can be used as a list or a dictionary. However, that's not all. Lua's Table has other uses:

  • Module (module)
  • Object (object)

Modules

In Lua, a module is essentially a Table. This Table contains all the functions and variables that the module intends to expose to the outside. When you use require to load a module, it returns this Table. This design is very concise because you don't need extra keywords or special syntax to define a module; you just create a Table and return it.

For example, suppose you have a file named aa.lua with the following content:

local aa = {}

function aa.ff()
  return "This is a public function."
end

aa.vv = 100

return aa

In this example, aa is a Table. We set ff and vv as key-value pairs in this Table so they can be accessed externally.

Objects

Lua's Table can also be used to implement Object-Oriented Programming (OOP), mainly through the metatable feature.

A Lua Table can contain a metatable, which, although also a Table, carries special semantics. When a Table containing a metatable is treated as an object and a method call is made, if the method cannot be found, it will automatically search within the metatable, thereby achieving key features of object-oriented programming such as inheritance and polymorphism.

Fennel Syntax Extensions for Tables

Influenced by Clojure, Fennel also brings some excellent designs from Clojure, leading to some special uses of Tables.

Colon-Prefixed Notation

If a container's key is a string type and does not contain spaces or reserved characters, the key can be written using colon-prefixed notation, :shorthand.

{:key value :number 531}

This colon-prefixed notation is especially common for Table keys. In the Clojure language, this colon-prefixed notation directly corresponds to a new data type called keyword, which is specifically used for container keys.

It is worth noting that in Fennel, this colon-prefixed notation is converted into Lua's string type after compilation; for example, :key becomes "key". This differs from Clojure's keyword, which is an independent data type, but it provides similar convenience.

Reading and Writing

When a Table's key uses colon-prefixed notation, values can be accessed using ${table}.${key}, which is more concise than (. ${table} ${key}).

(let [tbl {:x 52 :y 91}]
  (+ tbl.x tbl.y)) ; -> 143

Furthermore, the ${table}.${key} syntax can also be used with the set function.

(let [tbl {}]
  (set tbl.one 1)
  (set tbl.two 2)
  tbl) ; -> {:one 1 :two 2}

Destructuring Tables

  • For Sequential Tables, position-based destructuring can be performed.
(let [data [1 2 3]
      [fst snd thrd] data]
  (print fst snd thrd)) ; -> 1       2       3
  • For General Tables, key-based destructuring can be performed.
(let [pos {:x 23 :y 42}
      {:x x-pos :y y-pos} pos]
  (print x-pos y-pos)) ; -> 23      42

For key-based destructuring, if the variable name that accepts the destructured value is exactly the same as the key's name, an even more abbreviated syntax can be used: the key no longer uses "colon-prefixed notation" but is written directly as "colon".

(let [pos {:x 23 :y 42}
     {: x : y} pos]
 (print x y)) ; -> 23      42

Summary

The official Fennel website also has a Lua Primer document, which specifically explains the parts of Lua that the Fennel author considers important, with more content than this chapter.

I infer the Fennel author's intention, perhaps he believes that after developers read his Fennel Tutorials and Lua Primer, they will be sufficiently equipped to start using Fennel?