Devlog: UCL - Adding Some Missing Library Functions
Working on UCL, adding some missing builtins I’ve been finding myself wanting. Nothing too interesting. Just functions like strs:has-prefix, strs:trim-suffix, and other functions involving strings and lists that are missing. That sort of thing.
I am facing some decisions around strs:substr. See, the usual way these builtins are validating arguments is that if there are more arguments than are necessary, they are generally just ignored. One could describe this as the “JavaScript” approach to argument validation: open Node and evaluate "hello".substr(1,2,3,4,5,6,7,8,9) and one will get the same result as if they simply typed in "hello".substr(1,2). But now I’m wondering if it’s better to assert that the arguments are either one or two positions, along with the string. What if I wanted to add more positional arguments in the future?
Although in practice, why would I want to do that? Well, okay, I do have an idea of for taking multiple substrings in a single call. So could be useful? Ah, probably doesn’t matter: it’ll just be me using this for now.
Anyway, strs:substr. One deviation from JavaScript I think I would like to do is that if a single number is used, that would be treated as the end position of the substring; as oppose to JavaScript which treats it as the start position. So running strings:sub "hello, world" 5 will return "hello", rather than ", world". To start from the right, a negative position can be used: strings:sub "hello, world" -5 will return "world". Two numbers will be treated as a start (inclusive) and end (exclusive) positions, like most other languages; although both can be negative which will set the position from the right side.
strs:substr "hello, world" 5
--> "hello"
strs:substr "hello, world" -5
--> "world"
strs:substr "hello, world" 3 10
--> "lo, wor"
Okay, that’s working for strings. Need to do the same thing for lists. I could almost use the same function.
Oh, I keep forgetting that I need to worry about nils. How should sublists work if the passed in list is nil? Maybe it’s better to just return a nil. Less hassle than requiring the user to deal with nil values. I’ll start with that approach, but I may change that the future. Should I do the same for strs:substr? Hmm, no. I think I’ll keep the default binding rules for nil to strings, which is basically convert it into an empty string. That saves me from changing the binding logic. Maybe I should lock that decision in with a test.
Hmm, also, many of the other list builtins support iterators. If I were going to add iterator support to sublist , then how would I handle the negative numbers? Iterators, by definition, can be boundless. Maybe leave iterator support out for now.
lists:sublist [a b c] 1
--> [a]
lists:sublist [a b c] -1
--> [c]
It also looks like the argument binder has made my mind up about nils for me. I added the test which testing for nils, and I got an expected listable error.
lists:sublist () 1
--> error: expected listable
I think that’s fine. Might be better to fail on nils actually.
Getting back to the argument question raised earlier, I did run into an issue where I was using strs:split without a second argument. When one does that, UCL will do something similar to Go, and return a list of each individual rune. I think that’s a useful feature, but I think I’d prefer it if that option was explicit, and if a second argument were not set, raise an error.
Okay, a few more items on my todo list. I need a string replace function. I generally prefer such a function to replace all the instances of a string, rather than just the first; but that might be pushing my preference a little too far. So I’ll settle with replacing the first instance of a substring and add an -all flag to replace all instances.
Actually, no. Go has support for specifying a count, so I’ll use that by adding an -n COUNT option. If N is unset, then all the substrings are replaced.
strs:replace "hello hello hello" "hello" "world"
--> world world world
strs:replace "hello hello hello" "hello" "world" -n 1
--> world hello hello
Also, given that this is using Go’s builtin strings.Replace function, setting “match” to the empty string will cause replace to match between each rune, plus the start and end of the
string.
strs:replace "each one" "" "|"
--> |e|a|c|h| |o|n|e|
Could be useful, so I’ll keep this feature in.
Nearing the end now: adding an -in switch to os:exec to set stdin for an command. The original idea was to name the switch -stdin, and I also considered -input. But when writing the tests, I naturally settled on -in.
os:exec tr '[a-z]' '[A-Z]' -in "hello"
--> HELLO
It would’ve been nice to accept stdin from a pipe. Sadly, that’s not possible with how pipes actually work.
And the last one: adding a nil? builtin, which will return true if the given item is nil. I guess in the grand scheme of things, it’s probably not strictly necessary, as anything that’s not zero, including nil, will be true. And one could always use eq $thing () to test if $thing is nil. So I may leave this one on the backlog.
Okay, finished. I wanted to add some stuff to Dynamo Browse, but I ran out of time. I will update the version of UCL used by Dynamo Browse, but all that other stuff will need to wait for tomorrow.