about types #8

Here is my response to a series of questions raised about Fexl at GitHub on Mon 2026-10-26.

I've been exploring https://fexl.com/ and I have a fundamental question about the type system. I've read in several places (Rosetta Code, Hacker News) that Fexl does not have a built-in type inference system and that types are checked at run-time. I've also seen the author mention that strict typing is "pretty much impossible" due to the use of combinators (S, C, I, Y, etc.), since you can't assign a meaningful type to them.

It's true that full type inference of general lambda expressions, or expressions built from combinators such as S and C, is an uncomputable problem. I am also aware that type inference is possible within certain restrictions, such as in the Haskell language. However, I have chosen first to implement a functional style language with close ties to the underlying machine, allowing me to orchestrate C function calls but with full memory safety and without the hassle of programming everything directly in C.

Regarding type inference, my idea was that once I had that base language, I could then implement type inference in that language. However, I have not yet bothered to do so, because my main goal has been to implement working software to run my business, and I personally am comfortable with run time type checking. That's just me. It hasn't been a priority yet.

Ideally I'd like to go further than just type inference, and investigate using Fexl to "compile" Fexl code down to native C. However, even that has not risen to a priority for me, because every time I encounter any sort of performance bottleneck, I simply implement just that one thing in C and then move on.

Now on to your questions.

What is the list of types?

Is there a defined set of built-in types (e.g., long, string, function, etc.), or is the type system entirely ad-hoc based on the C implementation? Could you provide an overview of the primitive types that exist at runtime?

There is a defined set of built-in primitive data types:

bn A "big number," an integer of virtually unlimited size, either negative, positive, or zero.
boolean Either T or F (true or false)
buf A character buffer, to which strings of characters can be quickly appended in a mutable way.
file A file handle, i.e. FILE * in C.
istr An interator over a string. This maintains a position within a given string as you iterate through its bytes.
list Either null, or a value followed by another list. This is an immutable list. You can build new list values but you can't modify or delete any items in the list.
num A number, specifically a double-precision floating point number as codified in C, which is type double.
queue (Coming soon, in the works) This is a completely mutable list, which I am calling a "queue" even though it supports insertion and deletion at both ends and in between. I have found that in my accounting software where I am closing trade lots that I need a more efficient way of keeping large lists of lots. With immutable lists, I have to rebuild the list to do an insert or delete. When I start to see quadratic run time behavior like that, that's a good sign that I need to implement something in C.

I did the same thing for the record type, because using immutable assoc lists was fundamentally quadratic. When I reach a pain point, there I find low hanging fruit and I take it.

record A mutable record which maps keys to values. The keys can be either numbers or strings, and the values are arbitrary Fexl values, either data or function. The key-value pairs are stored in sorted order, and you can iterate through them by their numeric position in the record. Both number and string keys can be used within the same record, with number keys appearing before string keys in the sort order.
str An immutable string of characters (bytes)
tuple Very much like a list, except you can bind all the elements of a tuple into named symbols in a single operation, often known as "destructuring," for example:
{1 2 3} \x\y\z
# Now x=1, y=2, and z=3
Like a list, a tuple may have zero or more elements. Internally a tuple is so much like a list that you can switch between list and tuple views with a single operation (list_to_tuple and tuple_to_list).
var A mutable reference to an arbitrary value. A var contains a direct reference to a value of any kind, including data or function. You may change what the value points to, but that doesn't mean you can alter the internal structure of the value itself. For example, if a var points to a string, the string itself remains immutable.
void A value which in effect short-circuits any computation involving it, returning void all the way up through the computation wherever void is used.

Note that I do not include "function" as a data type. Both data and function are called "values" generally, but I define function as a value denoted by a lambda expression. In some cases data may be meaningfully used as a function, notably list, record, and tuple.

Is there a way to ensure types? For example, can I declare that a function like fun x y z only take "strings"? Is there any mechanism—even an optional annotation—to enforce or aproximate this kind of behavior, or is it purely the programmer's responsibility to check arguments before using them?

You can do this, but in practice I find it better to regard this as part of the "contract" for calling the function, and if you violate the contract, the function returns void. For example let's say your function fun simply adds up the lengths of the three strings:

\fun=
    (\x\y\z
    + (length x);
    + (length y);
    + (length z);
    0
    )

Now if I call that with three strings in the proper way, it will return the sum of the lengths. But let's say x and z are strings, but y is a number. Then the whole function returns void.

That happens naturally from the bottom up. The (length y) term returns void because the length function insists that its argument be a string. Then when you try adding that void to the other lengths, you get void again because the + function insists that both its arguments be numbers.

You can check this yourself wherever you think is appropriate, when you aren't sure that the caller is giving you clean sanitized arguments, for example:

\total=(fun x y z)
is_undef total (error "What are you doing??!!");
say ["Cool, the total is "total]

There may be cases where you do want to validate each and every input individually, for example if the inputs are coming from an external source such as user input:

\fun=
    (\x\y\z
    not (is_str x) (error "x must be a string");
    not (is_str y) (error "y must be a string");
    not (is_str z) (error "z must be a string");
    + (length x);
    + (length y);
    + (length z);
    0
    )

However, in my experience I will have already validated user inputs far upstream from calling something as low-level as fun. By the time I call it, I have already established that the contract is met.

Is there a way to assert types? I understand Fexl has "reflective features which allow you to examine types and the internal data structures at run-time". What are the specific functions for type assertion? For instance, I saw references to type_long and arg(type_long, x) in the C source. Are there equivalent Fexl-level functions (like type_is_string, type_is_function, etc.) that can be called to assert a type and fail or branch accordingly?

OK, if you are seeing anything about type_long in the C source, then you are looking at an ancient version of Fexl. I suggest you look at more recent versions.

A very long time ago I had a type_long for representing a long C value, but I don't do that any more. I could do it, but honestly I'm just using double for all ordinary arithmetic. That even works fine for normal integer values. If I truly need integer math, I'd rather use a bn (big number), where I don't have to worry about overflow and underflow.

But yes, there are functions to check for specific data types, such as is_str, is_num, is_bn, is_var, is_list, is_null, is_tuple, is_bool, etc. You can see examples where I do different things based on the type of data passed in, e.g. format.fxl .

Is there a way to create new types? Can a user define a new type within Fexl itself (e.g., a record type or a tagged union), or does creating a new type require writing a new C routine (fexl_X) and rebuilding the interpreter? The documentation mentions that "to write a new core function named 'X' within Fexl, you simply create an appropriate C routine named fexl_X". Does the same apply to types?are they constructed during the compilation of the Fexl interpreter (i.e., in C)? is it possible to create and manipulate types dynamically at runtime from within a Fexl script?

Again, I think you are looking at a painfully ancient version of Fexl. All that stuff about fexl_X is way obsolete by many years.

Basically you're asking about cooking up arbitrary memory layouts as you would in C, doing all of that within Fexl without rebuilding the interpreter, and accessing that memory the way C would. My immediate answer is: no way, not a chance. Stuff like this:

struct thing
    {
    int type;
    char name[128];
    union
        {
        unsigned long x;
        double y;
        };
    };

No way. In fact, that is exactly what C is for. First of all, if I was doing that, I would probably just use a Fexl record with a type field and a proper string for the name, and use "x" and "y" as fields in the record as appropriate to the type. Is that less efficient than the raw C version? Most certainly. Is it more flexible? Yes. But if I really really needed so much raw speed that it would justify that C struct, then that's what I'd do: code the thing in C and provide Fexl hooks to call the underlying C functions.

Having said that, I have toyed with a build of Fexl which is dynamically linked, so that I could have Fexl create some C or assembler code, compile it into a .so (shared library), and then run it directly in the same memory space as the running interpreter. However, I have not valued that ability enough yet to adopt it as the norm.

I would appreciate any clarification or pointers to documentation. I've checked the main page but haven't found (or i didnt catch it) a dedicated section on the type system, so perhaps I'm missing something.

Thank you for your help!

I tend to do documentation only when there's outside interest, which is pretty rare — but thank you for your interest!