Modularity is the ability to put code in separate files, allowing the code to be shared. Most programming languages use specific keywords to implement modularity. Fexl has no keywords, and is based entirely on functions (lambda expressions), so it defines modularity in terms of plain functions known as contexts.
Modularity in Fexl is based on the concepts of form and context.
Here is some more detail on the concepts.
A form is a piece of parsed Fexl code which may contain undefined names.
A closed form is a form which does not have undefined names, meaning that its value is fully specified.
An open form is a form which does have undefined names, meaning that its value is not fully specified.
There is a function is_closed which may be applied to a form, returning T if closed or F if open. This is not commonly used, but it can be handy for meta-level tasks such as dynamic loading or interactive checking.
A form can be read from a file or stream with a parsing function, namely:
use_file | Read a form from a file named by a full path. |
use | Read a form from a file named by a path within the local directory where the script is running. |
parse | Read a form from either an open file handle, a string, or an open string handle (istr). |
A form can also be specified inline with the \; token. This is a very important feature and I will illustrate how it is actually used below. In the meantime here is a quick example:
# Anything after the \; within the scope is parsed and checked for syntax, # but no names in the form are yet defined and no evaluation occurs. \form= (\; \x=(* 2 3) say ["x = "x] fred wilma barney betty )
A context is a function which supplies definitions for zero or more undefined names in a form.
The simplest context is just the identity function I, or (). That context defines zero names when applied to a form.
The def function defines a single name. It takes a name, a value, and a form, and returns a new form with the name defined as the value. The original form is unchanged.
The std function defines a standard set of predefined symbols in a form such as say, put, map, filter, T, F, +, -, and many others.
Here is a context that takes a form and defines two names fred and wilma:
\form def "fred" (say "I am Fred."); def "wilma" (say "I am Wilma."); form
Here is a context that defines the names barney, betty, square, and distance. Here I have lifted the values up into separate named symbols, and I also list the names alphabetically. None of that is necessary, but it is the common practice.
# Return the square of a number. \square=(\x * x x) # Return the distance of a point x,y from the origin. \distance=(\x\y sqrt (+ (square x) (square y))) # Here I use \\ to avoid evaluating the side effects immediately. \\barney=(say "I am Barney.") \\betty=(say "I am Betty.") \form def "barney" barney; def "betty" betty; def "distance" distance; def "square" square; form
Contexts may also be easily chained together to combine sets of definitions. Here's an illustration:
# Here I have grouped the math-oriented names into a separate context. \cx_math= ( # Return the square of a number. \square=(\x * x x) # Return the distance of a point x,y from the origin. \distance=(\x\y sqrt (+ (square x) (square y))) \form def "distance" distance; def "square" square; form ) # This context defines fred and wilma. \cx_1= (\form def "fred" (say "I am Fred."); def "wilma" (say "I am Wilma."); form ) # This context defines barney and betty. \cx_2= ( \\barney=(say "I am Barney.") \\betty=(say "I am Betty.") \form def "barney" barney; def "betty" betty; form ) # This context combines all the definitions above. Note that cx_2 takes # priority, then cx_1, and finally cx_math. \cx_all= (\form cx_math; cx_1; cx_2; form )
The value function evaluates a form. If the form has any undefined symbols, it prints all the undefined symbols to stderr and dies. Otherwise it returns the fully resolved expression in the form, which is then evaluated normally.
As an example, let's say you're writing some code related to the old Flintstones cartoon. You can start by putting everything in one file:
main.fxl:
\\fred=(say "I am Fred.") \\wilma=(say "I am Wilma.") \\barney=(say "I am Barney.") \\betty=(say "I am Betty.") say "Meet the Flintstones." fred wilma barney betty
Note that I use \\ instead of \ for those definitions because I don't want to evaluate them immediately at the point of definition, since they have side effects.
Now you'd like to move those function definitions into a separate library file called "flintstones.fxl". Here's what you do. Cut the definitions out of the main file and paste them into the new "flintstones.fxl" file. Then add a series of def calls to create a context which defines those names:
flintstones.fxl:
\\fred=(say "I am Fred.") \\wilma=(say "I am Wilma.") \\barney=(say "I am Barney.") \\betty=(say "I am Betty.") \form def "fred" fred; def "wilma" wilma; def "barney" barney; def "betty" betty; form
Now edit your main file so it looks like this:
main.fxl:
\cx_flintstones=(value; std; use "flintstones.fxl") value; std; cx_flintstones; \; say "Meet the Flintstones." fred wilma barney betty
That first gets the Flintstones context cx_flintstones by reading a form from the "flintstones.fxl" file, applying the std context to it, and then evaluating it with value.
It then applies cx_flintstones to the inline form starting with \; and continuing to end of file. That context resolves the undefined symbols fred, wilma, barney, and betty, returning a new form.
It then applies the std context to that form, resolving the undefined symbol say, and returning a new form.
At that point the form is closed (fully defined), and it applies the value function to the form to evaluate it.
You can even test modularity within the main file itself. For example, let's say you want to define dino on the fly, without putting it in the Flintstones library:
main.fxl:
\cx_flintstones=(value; std; use "flintstones.fxl") value; std; cx_flintstones; def "dino" (say "I am Dino"); \; say "Meet the Flintstones." fred wilma barney betty dino
It is also possible to create custom environments that don't have to be included explicitly. This would allow you to write the main file like this, as if the entire Flintstones environment was built into the language:
main.fxl:
say "Meet the Flintstones." fred wilma barney betty dino
To do that you would need some higher-level script, let's call it "run.fxl", that sets up the environment and calls main.fxl:
run.fxl:
\cx_flintstones=(value; std; use "flintstones.fxl") value; std; cx_flintstones; def "dino" (say "I am Dino"); use "main.fxl"
That's the same as the previous example, except instead of using an inline form starting with "\;", you call use to read it from "main.fxl".
To make it possible to run files other than main.fxl, you can look at the command line arguments (argv), or at the HTTP parameters in the case of a web application, and use any file designated by the caller (with suitable security checks of course). For example:
run.fxl:
\cx_flintstones=(value; std; use "flintstones.fxl") value; std; cx_flintstones; def "dino" (say "I am Dino"); use_file (default ""; argv 2)
The (default "") ensures that the script is read from stdin if the caller did not specify a file.
I use this technique extensively in web servers, where I have a variety of files to serve different types of requests, but I don't want to have to establish the context in every single file. That way I can establish common environments shared by several files.
If I need to make an exception, that's easy to do. For example I might want to evaluate a particular file in a highly restricted context, to avoid possible harm from a user-specified script, or from any script that does not explicitly require a particular dangerous power.
Note in the above examples that the cx_flintstones context does not define itself. Consequently you cannot do this:
\cx_flintstones=(value; std; use "flintstones.fxl") value; std; cx_flintstones; \; say "Hello world." fred wilma # Now read a file that only needs std. \A=(value; std; use "A.fxl") # Now read a file that needs both std and cx_flintstones. # =====> OOPS! The name "cx_flintstones" is not defined here!! \B=(value; std; cx_flintstones; use "B.fxl")
You can remedy that by extending the cx_flintstones name explicitly:
value; std; cx_flintstones; def "cx_flintstones" cx_flintstones; \; say "Hello world." fred wilma # Now read a file that only needs std. \A=(value; std; use "A.fxl") # Now read a file that needs both std and cx_flintstones. \B=(value; std; cx_flintstones; use "B.fxl")