Decker v1.55 includes a subtle but impactful semantic change to Lil: the introduction of a nil
type.
In an attempt to minimize the number of moving parts in Lil, I originally designed it around a convention of using the number 0
as a signifier for nothingness. Undefined variables contained 0
. Out-of-bounds indexing (“outdexing”) produced 0
. A data structure needed to be extended or padded? 0
.
(11,22,33) @ -1,2,5
# (0,33,0)
image[5,5][-1,-1]
# 0
For many applications, this convention was fine, but it provided its own host of foot-guns. If outdexing producing a 0
value wasn’t contextually appropriate, elaborate bounds-checks were necessary, bloating bytecode and tempting subtle fencepost errors:
if min(!pos<0)&(pos<img.size) img[pos] else 1 end
Data interchange formats often contain malformed or missing data; using ordinary “zero values” like 0
and ""
hides these upon ingestion, making them difficult to filter, replace, or otherwise observe. This design added sharp corners to primitives and standard library functions alike:
"%i,%i" parse "12,apple"
# (12,0)
readcsv["a,b,c\n11,22\n44," "iii"]
# +----+----+---+
# | a | b | c |
# +----+----+---+
# | 11 | 22 | 0 |
# | 44 | 0 | 0 |
# +----+----+---+
Lil now has a nil
value for clearly and singularly representing nothingness. In a stricter language with a more trigger-happy conception of failure, introducing nil
might be considered a billion-dollar mistake, but I hope to convince those of you who haven’t already stormed off in a huff that nothing can be quite useful.
You can find nil
inside any undeclared variable. If the name “nil” is too terse or flavorless for your taste, consider spicing up your codebase with something more flavorful; Lil doesn’t mind:
nil=nil,zilch,nada,bupkis,0,1
# (1,1,1,1,0,0)
You can also find a nil
by outdexing, as a fill-value, or as an indicator of missing data:
(11,22,33) @ -1,2,5
# (nil,33,nil)
update job:"Doorstop" where name="Bob" from insert name with "Alice" "Bob" "Cindy" end
# +---------+------------+
# | name | job |
# +---------+------------+
# | "Alice" | nil |
# | "Bob" | "Doorstop" |
# | "Cindy" | nil |
# +---------+------------+
"%i,%i" parse "12,apple"
# (12,nil)
readcsv["a,b,c\n11,22\n44," "iii"]
# +----+-----+-----+
# | a | b | c |
# +----+-----+-----+
# | 11 | 22 | nil |
# | 44 | nil | nil |
# +----+-----+-----+
For the simplest cases, Lil’s squishy type-coercion rules come into play. If we need a number, a string, or a list, nil
behaves like 0
, ""
, or ()
, respectively. If these zero-values are contextually appropriate, there’s nothing to fix. Indexing into nil
produces nil
. Applying arguments to nil
likewise produces nil
. In many languages a “nil” or “null” is a value for which virtually no operations are permitted; for a Lil nil
, every operation is permitted.
sum 2,nil,nil,3,nil
# 5
nil.someAttribute
# nil
nil[]
# nil
If theres a chance we have an undesirable nil
in hand, we can use unless
to supply a replacement; it’s much simpler (and more obviously correct) to perform a substitution for a nil
than to perform manual bounds-checking:
1 unless img[pos]
In the vector case, the new fill
operator lets us perform a similar substitution at depth, over an entire list, dictionary, or table:
100 fill (0,3,nil,6,nil)
# (0,3,100,6,100)
"n/a" fill readcsv["a,b,c\n11,22\n44," "iii"]
# +----+-------+-------+
# | a | b | c |
# +----+-------+-------+
# | 11 | 22 | "n/a" |
# | 44 | "n/a" | "n/a" |
# +----+-------+-------+
And of course we could instead filter out or count the nil
values, depending on the context:
t:insert name job with
"Alice" "Programmer"
"Bob" "Doorstop"
"Cindy" nil
"David"
end
unemployed:sum nil=t.job
# 2
update job:"Life-Coach" where job=nil from t
# +---------+--------------+
# | name | job |
# +---------+--------------+
# | "Alice" | "Programmer" |
# | "Bob" | "Doorstop" |
# | "Cindy" | "Life-Coach" |
# | "David" | "Life-Coach" |
# +---------+--------------+
Enjoy, lilateers- from this point on you’ll have nothing to worry about!