So Much Value

  • Home
  • Back
  • Complexity is a backdoor and can be misinterpreted as sophistication.

    asvln.com

    Values

    As covered in my last post, I’m working on Andy, my personal shell. One of the pretty interesting features of Andy I think is the way that values are handled. In the conventional shell, you can think of a value as a string. In echo foo, both ‘echo’ and ‘foo’ can be thought of as values. Values aren’t just string-literals though, a Bash process-redirection (<(cmd)) is also a value; typically one that looks like ‘/dev/fd/N’.

    In Andy this mostly holds, with one very important difference: values are not strings — they’re lists of strings. While the string-literal "foo" might be a string list of length 1, the process-redirection of <>{cmd} is a list of 2 strings — a path to a file you can read from, and a path to a file you can write to.

    This is an important distinction to make because of the fact that value-concatenation goes from a simple string-concatenation to a cartesian product of the left- and right values:

    # foo.c foo.h bar.c bar.h baz.c baz.h
    echo (foo bar baz)(.c .h)
    # Or alternatively:
    echo (foo bar baz).(c h)

    This is not just constrained to literals of course, process substitutions are also values, so appending each line of a processes output with a closing-angle-bracket becomes trivial:

    thomas ~ # `(D…){P…} is syntax for splitting a process P’s
    thomas ~ # output on the delimiters D.
    thomas ~ printf %s\n '> '`(\n){grep MHz /proc/cpuinfo}
    >cpu MHz		: 1127.333
    >cpu MHz		: 1199.957
    >cpu MHz		: 1139.098
    >cpu MHz		: 1200.000
    >cpu MHz		: 1172.249
    >cpu MHz		: 1199.957
    >cpu MHz		: 1151.501
    >cpu MHz		: 1199.996

    Of course you can also append the closing-angle-bracket using printf itself, but it’s just an example after all.

    Dynamic Code

    What makes Andy different is not just the behavior of values, but where you can use them. In most shells, various syntactic elements are not expanded in the same way that regular values are. The following example results in an error because the function name $foo doesn’t get expanded into bar:

    thomas ~ foo=bar
    thomas ~ $foo() { :; }
    bash: `$bar': not a valid identifier

    Andy doesn’t give a fuck though, it lets you do this if you really want to:

    thomas ~ set foo bar
    thomas ~ func $foo {}
    thomas ~ type bar
    function

    Now you might think that this is really retarded — especially if you’re a Rust developer — and you know what? I did too! But you know what the beauty of recreational programming is? It doesn’t have to be good — and once you learn to ignore the low-value morons that will see you experimenting with new ideas and call it garbage — you can start to actually develop some pretty neat solutions to problems.

    For example, Andy like all shells implements support for signal handling. In the POSIX shell this is done via the ‘trap’ builtin function:

    trap "rm -f foo.tmp bar.tmp" QUIT EXIT

    Now personally, I really don’t like trap. Having to write your signal handling code in string is just not very ergonomic. You can put your code in a function that you then call in your signal handler, but I think Andy can do better. The way you handle a signal in Andy is to simply define a function of the name ‘sigX’ where ‘X’ is the name of the signal. If you want to write a handler for SIGINT, you simply define the ‘sigint’ function!

    What if we want to handle multiple signals though? In the above example, we deleted some temporary files on SIGQUIT and also on the special SIGEXIT (a fake signal that signifies that the process is exiting). Well the naïve solution is to simply define a handler function that you call from your handlers:

    func handler {
    	rm -f (foo bar).tmp
    }
    
    func sigquit { handler }
    func sigexit { handler }

    But wait… remember how we can dynamically define functions? What if we just… dynamically define our signal handlers:

    # $_ represents the current iteration variable.  You can give an explicit name
    # with a for-in loop though.
    for quit exit {
    	func sig$_ {
    		rm -f (foo bar).tmp
    	}
    }

    Personally, I think that’s pretty neat, and I like it! Of course some Haskell developer that’s never actually written any working software will probably be sending me an angry email right about now about how this is unsafe code and blah blah, but I don’t care.

    I suppose while this post is primarily about just showing off some of Andy’s behavior, I also want to make the point that it’s always a good idea to just try new ideas, no matter how contrary they are to ‘best practices’ and Rust developers. Don’t waste your time worrying about what other people think about what you make; I used to do that, and it’s not great. Just make shit, even if it’s weird. You might just accidentally add a cool new feature you actually kind of like.