Hunter Praska's Blog

Reply to thread

hunter ###Admin 2019-03-11 09:58:43 No. 5

Using Chez Scheme as a Shell

For a while now I've wanted to use Scheme as my shell. There have been a bunch of projects with a similar goal, but they all do one thing wrong: they try to keep shell syntax. They try to make it easy to run shell commands alongside a more general programming language and they end up with some Frankenstein language that's in between. I didn't want any shell, I wanted pure Scheme.

I felt that existing Scheme implementations were lacking some necessary features mostly tab-completion of filenames, but a few other things so I began work on my own. I learned a lot and I felt that it was going well. However, a few months ago I found that Chez Scheme supported tab-completion of filenames out of the box; so I decided to try it out.

The Good

I think that Scheme handles complex commands much better than bash does. As an example, recently I wanted to add foreign subtitles to a TV show I had. I had the foreign subs, but the timing was off so I decided to extract the native subtitles and use their timing. I wanted the extracted subtitles to share the same name as the video, but in bash this is a little wonky. It is certainly possible to do, but I can never remember the syntax Can you? Every time I do anything more complex than run a command in bash I have to look up the syntax for it. To make it a bit harder, I had manually done this for the first video and added the subtitles so I needed to ignore two of the videos. In Scheme this is trivial:

;; We use the threading macro, like a bash pipe
(-> (ls) ;; Grab a list of files in the current directory
    ;; select only files that end with mkv
    ((lambda (files) (filter (lambda (file) (string-ends-with? file "mkv")) files)))
    ;; ignore the first two files that were done manually
    ((lambda (files)
       ;; run mkvextract on each video selecting only the subtitles (track 4)
         (lambda (file)
           (run-command "mkvextract"
                        "name the subtitles VIDEO-NAME.ass
                        (string-append "4:"
                                       ;; remove "mkv" from the file name
                                       (substring file 0 (- (string-length  file) 3))

The Bad

Really I think that it is very nice. The only really bad aspect is the lack of libraries. The standard library is abysmal, I'm used to writing Rust code with a very good standard library and for everything else. With Scheme, I had to write procedures like string-begins-with myself. There also really aren't third party libraries. I was able to use irregex which is quite nice, but that's about it.

Another problem is that tab completion isn't very spectacular. It works okay for procedure names and filenames I think it works as bash does, but I'm used to zsh where I can type ~/P/P<tab> and it expands to ~/Programming/Projects. but there isn't any help with arguments. I found that I kept forgetting how many arguments a procedure expected and what order the arguments were. Types might solve some of this, but a more general solution might be to display the unfilled arguments' names. Idris can do some interesting things with autocompletion too. Some of this might just be a lack of familiarity with the system.

My threading macro is also quite annoying. It places the previous result as the first argument to the next procedure; if you need it in a different position you have to wrap the procedure in a lambda, and the lambda expression must be wrapped in parentheses such that it is being applied. Racket has a threading macro that avoids these issues, allowing you to specify the argument position with an underscore. It might be possible to do this in Scheme but I'm not very good with macros.

Final thoughts

I'm not really using this as my daily shell. What I've been doing is firing it up when I need to do something complex and I've found that this gives good results.

What I'd really like to see is for Racket to support tab-completion of filenames. Racket has a great threading macro, a good standard library, and a good community around it. Racket is in the process of migrating to Chez Scheme, so I was hoping they would gain support. Having checked the current build of RacketCS there is not support for completion of filenames which is disappointing.

I might continue to tinker with it, but I don't really think UNIX systems are suited for user interaction. I think that I will work towards greener pastures.

Anonymous 2019-03-11 10:54:50 No. 6

Hi! I use my own threading macro in Racket, and it might just work directly in Scheme:

;; macro that lets one compose functions with any number of parameters
;; each composed expression is essentially curried to accept one parameter 'x' through the use of internal lambdas
;; the result of each composed expression (evaluated right-to-left) is passed on to the next through 'x'
(define-syntax (composex stx)
(syntax-case stx ()
((_ f1 ...)
(with-syntax ([x-var (datum->syntax stx 'x)]) ; create a local variable so Racket doesn't scream it doesn't exist
#'(compose1 (λ (x-var) f1) ...))))) ; move each composed expression inside a lambda that uses our variable

; example:
(check-equal? ((composex (string-replace x " " "-")
(string-downcase x)
(string-trim x)) " Naice Day ")

Anonymous 2019-03-11 10:56:44 No. 7

Hi again, I also made a Racket shell that does most of what you want, but still keep compatibility with basic shell functions:



Anonymous 2019-03-11 14:15:14 No. 9

If you're looking for more complicated threading functionality, I wrote a library for R6RS that allows you to specify positions and does short-circuiting.

Anonymous 2019-03-12 03:25:27 No. 10

Something you can look into is swank/slime editor integration, e.g. in Vim or Emacs.

A text editor is not that different from a line-editor. S-expressions are much more useful in an editor that can send parts of a big expression to repl for evaluation, as well as have all the cursor movement/refactoring goodness that comes from using highly structured forms. This includes easy documentation lookup as well as automatic argument information window.

Emacs has the advantage of having the built-in language to be a lisp, so when using elisp alone it is kind of like a multiline lisp repl. Vim can do the same for vimscript, python, ruby, perl, lua, tcl. As in, evaluate lines that would affect the editor state (e.g. cwd).

When working with an external repl via editor integration (e.g. common lisp or clojure), the state is detached from the editor itself. Which is still like a multiline repl, except you have a separate set of rules to modify the editing environment.