-
Notifications
You must be signed in to change notification settings - Fork 14
/
readme.mdpp
1209 lines (947 loc) · 49.3 KB
/
readme.mdpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
ECMAchine
=========
ECMAchine is an in-browser Scheme REPL that is also a toy operating system. It has a virtual filesystem that is accessed through Unix-like commands, as well as a rudimentary process management system.
**Why?** I made ECMAchine for a few reasons. For one, it's an interesting experiment in what a filesystem based around S-expressions could look like. It was also a good exercise in writing an interpreter and finally applying all those things I learned in SICP. Most importantly, though, ECMAchine is simply a lot of fun. I've spent countless hours playing around with it and coming up with little programs for it, and hopefully by the end of this tutorial you, the reader, will be able to get a sense for how fun it is to work with ECMAchine.
This tutorial will walk you through the file and process management features of ECMAchine, and then show a bunch of examples of cool things that you can do with them. It is aimed at people with at least a little bit of experience with Scheme or another Lisp dialect.
Table of Contents
-----
!TOC
The REPL
-----
ECMAchine supports the expected Scheme arithmetic commands, list-processing commands, and control structures:
```
ecmachine:/ guest$ (+ 1 2 3 4)
10
ecmachine:/ guest$ (- (/ 12 3) 2)
2
ecmachine:/ guest$ (cons 'a '(b c))
(a b c)
ecmachine:/ guest$ (list 1 2 (list 3 4))
(1 2 (3 4))
ecmachine:/ guest$ (car '(1 2 3))
1
ecmachine:/ guest$ (cdr '(1 2 3))
(2 3)
ecmachine:/ guest$ (if (= 5 3) 'a 'b)
b
ecmachine:/ guest$ (cond
.. ((> 5 0) 'positive)
.. ((< 5 0) 'negative)
.. (#t 'zero))
positive
```
Of course there are closures, definitions, and binding constructs:
```
ecmachine:/ guest$ (lambda (arg) (+ arg 1))
(λ (arg) (+ arg 1))
ecmachine:/ guest$ ((lambda (arg) (+ arg 1)) 5)
6
ecmachine:/ guest$ (define (square x) (* x x))
ecmachine:/ guest$ (square 5)
25
ecmachine:/ guest$ (begin
.. (define x 5)
.. x)
5
ecmachine:/ guest$ (let ((x 2) (y 3))
.. (* x y))
6
```
And we can do fun things like recursively computing Fibonacci numbers:
```
ecmachine:/ guest$ (define fib
.. (lambda (n)
.. (cond
.. ((or (= n 0) (= n 1)) 1)
.. (#t (+ (fib (- n 1)) (fib (- n 2)))))))
ecmachine:/ guest$ (fib 10)
89
```
But we already know about Lisp. What makes ECMAchine different?
Introspection
-----
You can view all of currently defined variables in the global environment with the `environment` command.
```
ecmachine:/ guest$ (environment)
(!= * + - / < <= = > >= __fileSystem abs and append cadr car cd cdr cons cp dir? do-nothing else environment exec fact file? filter help inspect-primitive intersperse js-apply kill kill-all length
list ls map math mkdir mv new newline nil not null? or overlay path peek perfmon.header perfmon.perfInfo performance processes read rm save size smile sort start sum time)
```
Let's take a look at the absolute value function `abs` in order to see the three levels of functions in ECMAchine:
```
ecmachine:/ guest$ abs
(λ (x) (cond ((> x 0) x) (#t (- x))))
ecmachine:/ guest$ -
#<Function ->
ecmachine:/ guest$ (inspect-primitive -)
function (args) {
if (args.length == 1) {
return (- args[0]);
} else {
return (args[0] - args[1]);
}
}
ecmachine:/ guest$ cond
[USER]: Eval Error: Unbound variable cond
```
When we type in `abs` into the evaluator we get to see the underlying Scheme representation, so we know that `abs` is defined as a Scheme function. (It's actually defined in `/startup/math.lsp` and thus loaded at startup, but more on that later.) We can see that `(abs x)` simply outputs `x` if `x > 0` and `-x` otherwise, but then let's say we wanted to dig a little deeper and figure out how some of the functions used in this definition are themselves implemented.
When we type `-` into the evaluator we get `#<Function ->`, which means that `-` is a primitive function for ECMAchine. We can then use the `inspect-primitive` function to view the (read-only) underlying JavaScript representation of `-`. Finally, when we type `cond` into the evaluator we get an Unbound variable error, signifying that `cond` is not actually a function at all but a language construct.
On the topic of introspection, we can even examine the contents of running processes with the `peek` command (more on processes later). For instance, here we're examining the source code of a currently-running clock application:
```
ecmachine:/ guest$ (processes)
((-1 Terminal) (0 clock.app) (1 processmonitor.app) (2 memorymonitor.app))
ecmachine:/ guest$ (peek 0)
(overlay (time (list 'h ': 'm ': 's)) -30 -30 'clock)
```
The File System
-----
### Folders
The file system is navigated using the `cd` and `ls` functions. The preferred method for concatenating file/directory names into paths is with the `path` function, as shown below:
```
ecmachine:/ guest$ (ls)
(apps cleanup.s readme.txt startup usr)
ecmachine:/ guest$ (ls 'apps)
(clock.app memorymonitor.app processmonitor.app unixclock.app virushunter.app)
ecmachine:/ guest$ (cd 'usr)
ecmachine:/usr guest$ (ls)
()
ecmachine:/usr guest$ (mkdir 'usr2)
(Directory /usr/usr2 created)
ecmachine:/usr guest$ (ls)
(usr2)
ecmachine:/usr guest$ (cd '..)
ecmachine:/ guest$ (path 'usr 'usr2)
usr/usr2
ecmachine:/ guest$ (cd (path 'usr 'usr2))
ecmachine:/usr/usr2 guest$
```
### Files
The `read` function is used to read the contents of a file, while `save` and `append` create a new file and append data to an existing file, respectively.
```
ecmachine:/ guest$ (ls)
(apps cleanup.s readme.txt startup usr)
ecmachine:/ guest$ (read 'readme.txt)
ECMAchine by Alex Nisnevich. Thanks to Jakub Jankiewicz for his jQuery Terminal plugin (distributed under LGPL).For more information check out the git repo at https://github.com/AlexNisnevich/ECM
Achine
ecmachine:/ guest$ (save 'blah.txt '(I am using ECMachine!))
(Saved file /blah.txt)
ecmachine:/ guest$ (read 'blah.txt)
(I am using ECMachine!)
ecmachine:/ guest$ (append 'blah.txt '(Hooray!))
(Updated file /blah.txt)
ecmachine:/ guest$ (read 'blah.txt)
(I am using ECMachine!)
(Hooray!)
```
### Other File System Commands
You can move, copy, and delete files and directories with the `mv`, `cp`, and `rm` commands, respectively.
The predicates `file?` and `dir?` can be used to find if a path points to a file, directory, or neither.
Review: Higher-Order Functions
-----
Before we continue into scripts and processes, let's define two functions that we'll be using a lot later: `map` and `filter`. (In fact, these functions are so important that in ECMAchine, they're both defined in `/startup/mapreduce.lsp` and loaded at startup.)
### Map
`map` takes another function and maps it to a list, executing it for every element of the list and combining the results into a new list:
```
ecmachine:/ guest$ (define (map proc items)
.. (if (null? items)
.. nil
.. (cons (proc (car items))
.. (map proc (cdr items)))))
ecmachine:/ guest$ (map abs '(1 -3 5 -6 0))
(1 3 5 6 0)
ecmachine:/ guest$ (map length '(hello lisp))
(5 4)
```
### Filter
`filter` takes a predicate (that is, a function that returns a boolean value) and filters all the elements of a list that satisfy the predicate:
```
ecmachine:/ guest$ (define (filter pred seq)
.. (cond ((null? seq) nil)
.. ((pred (car seq))
.. (cons (car seq)
.. (filter pred (cdr seq))))
.. (#t (filter pred (cdr seq)))))
ecmachine:/ guest$ (filter (lambda (x) (> x 5)) '(3 7 2 0 9 15))
(7 9 15)
ecmachine:/ guest$ (filter (lambda (x) (= (length x) 3)) '(the blue cat))
(the cat)
```
A Few More Functions
-----
We're almost at the fun part of the tutorial, but before we get there, I should briefly mention a few more important functions that are included as primitives in ECMAchine.
### js-apply
```(js-apply func [obj] args)``` is one of the most powerful primitives in ECMAchine, because it allows you to use almost any function in JavaScript's standard library. In its two-argument form, it evaluates ```func(args)``` in JavaScript - in this case, ```func``` is usually a the name of a Javascript library method like ```Math.sin```. In its three-argument form, it evaluates ```obj.func(args)```, where ```obj``` is generally a string or a list.
Some examples:
```
ecmachine:/ guest$ (js-apply 'alert '(Hello!))
{displays a dialog}
ecmachine:/ guest$ (js-apply 'replace 'the-quick-brown-fox '(fox lisp))
the-quick-brown-lisp
ecmachine:/ guest$ (js-apply 'split '/usr/usr2/file '/)
( usr usr2 file)
ecmachine:/ guest$ (js-apply 'Math.sqrt 100)
10
```
That last use case is so common that we can simplify it a little with a helper function:
```lisp
(define (math func args)
(js-apply (+ 'Math. func) args))
```
Now we can call JavaScript mathematical commands like this:
```
ecmachine:/ guest$ (math 'sin 1)
0.8414709848078965
```
### length
```
ecmachine:/ guest$ (inspect-primitive length)
function (args) {
return args[0].length;
}
ecmachine:/ guest$ (length 'thisIsAString)
13
ecmachine:/ guest$ (length '(this is a list))
4
ecmachine:/ guest$ (length (read 'readme.txt))
201
```
```(length obj)``` returns the length of an array or string, using JavaScript's `length` property. One common use for it is to retrieve the length of a file.
### sort
```
ecmachine:/ guest$ (inspect-primitive sort)
function (args) {
// This doesn't NEED to be a primitive, but it's a pain to implement,
// and I'd rather use JavaScript's underlying sort
// Usage: (sort lst [keyfunc])
return args[0].sort(function (a, b) {
var keyA = (args.length > 1) ? lispApply(args[1], [a]) : a;
var keyB = (args.length > 1) ? lispApply(args[1], [b]) : b;
if (keyA < keyB)
return -1;
if (keyA > keyB)
return 1;
return 0;
});
}
ecmachine:/ guest$ (sort '(5 3 8 2 4 1))
(1 2 3 4 5 8)
ecmachine:/ guest$ (sort '(the quick brown fox))
(brown fox quick the)
ecmachine:/ guest$ (sort '(5 3 8 2 4 1) (lambda (x) (- x)))
(8 5 4 3 2 1)
ecmachine:/ guest$ (sort '((5 apples) (3 bananas) (8 oranges) (2 pears) (4 peaches) (1 watermelon)) (lambda (x) (car x)))
((1 watermelon) (2 pears) (3 bananas) (4 peaches) (5 apples) (8 oranges))
```
```(sort lst [keyfunc])``` sorts a list in ascending order. If a second parameter is specified, it is a key function that is applied to the list's elements before sorting.
### time
```
ecmachine:/ guest$ (inspect-primitive time)
function (args) {
var date = new Date();
if (args[0] == null) {
return date.getTime();
} else {
return args[0].map(function (str) {
switch (str) {
case 'h':
return date.getHours();
case 'm':
return date.getMinutes();
case 's':
return date.getSeconds();
default:
return str;
}
});
}
}
ecmachine:/ guest$ (time)
1332744204185
ecmachine:/ guest$ (time '(h : m : s))
(23 : 43 : 41)
```
```(time)``` returns the current time, either in the default JavaScript time format (that is, Unix time in milliseconds), or formatted with an optional format string. We will use it later to build clocks.
### newline
```(newline)``` inserts a newline character. One use of it is in the Process Manager application described later, where it's interspersed within a list to put each list element in a new line.
Scripts
-----
A **script** is an executable Lisp file. Scrips are created like any other file, and by convention end in the file extension `.s`. The `exec` command is used to execute scripts.
For example, `/cleanup.s` is a script that cleans the contents of the `usr` directory. Let's take a look at it and then run it.
```
ecmachine:/ guest$ (read 'cleanup.s)
(rm (path '/ 'usr))
(mkdir (path '/ 'usr))
ecmachine:/ guest$ (ls 'usr)
()
ecmachine:/ guest$ (save (path 'usr 'myFile) 'blahblahblah)
(Saved file /usr/myFile)
ecmachine:/ guest$ (ls 'usr)
(myFile)
ecmachine:/ guest$ (exec 'cleanup.s)
(Directory /usr created)
ecmachine:/ guest$ (ls 'usr)
()
```
Fairly straight-forward: `/cleanup.s` removes the `/usr` directory, clearing its contents, and then recreates the directory.
_As an aside, could we delete the contents of a directory without deleting the entire directory? We could, but it would be a little more complicated:_
```lisp
(map rm (map (lambda (x) (path 'usr x)) (ls 'usr)))
```
### Special Types of Scripts
There are two more important types of scripts, that have their own extensions by convention.
- **Libraries** (by convention ending in the extension `.lsp`) consist only of function definitions - for example, `map` and `filter` are defined in the library file `/startup/mapreduce.lsp`.
- **Shortcuts** (by convention ending in the extension `.lnk`) consist only of `exec` or `start` function calls to other scripts or applications (more on those later).
Why would we need shortcuts? One big reason has to do with the `/startup` directory: when ECMAsystem launches, every script located within the `/startup` directory is executed. Generally, the startup directory is expected to consist of only libraries and shortcuts.
Now, scripts are cool, but wouldn't it be cooler if we could somehow run a script continuously in the background? This is where processes come in.
Processes
-----
A **process** is created when a script is called with the `start` function. `(start path interval)` begins running the script at _path_ with a refresh rate of _interval_ milliseconds. By convention, scripts that are meant to be run as processes are called **applications** and end in the file extension `.app`.
### Overlays
Since processes run in the background, the terminal generally isn't the best place for them to send their output - ideally, a process would be able to output directly to some part of the screen. Fortunately, **overlays** exist for just this purpose.
An overlay is created with the `overlay` command as follows:
```lisp
(overlay '(text to display) x-pos y-pos 'nameOfOverlay)
```
The overlay is placed at position `(x-pos, y-pos)`, where `x-pos` counts pixels from the left if positive and from the right if negative and likewise `y-pos` counts pixels from the top if positive and from the bottom if negative. For example, if `(x-pos,y-pos) = (-30, -30)`, the overlay would be placed . In all of my examples, `x-pos` is always negative, and I don't really see much use for left-aligned overlays in general because they would get in the way of the terminal.
Only one overlay with a given name can exist at any time - creating a new overlay with the same name as an existing one has the effect of replacing the old overlay with the new one. This means that if a processs continually calls the `overlay` function with the same name parameter, the overlay is continually updated.
Let's use what we've learned about processes and overlays to make a clock application.
### Example: A Simple Clock
All it takes to make a clock is:
```lisp
(overlay (time (list 'h ': 'm ': 's)) -30 -30 'clock)
```
This gets the current time (in `HH:MM:SS` format) and displays it in the lower-right corner, 30 pixels from each side. If we save this as `/apps/clock.app` we can run it as a process like this:
```
ecmachine:/ guest$ (start (path 'apps 'clock.app) 1000)
(Starting process at apps/clock.app with PID 4)
```
### Process Management
Now that we've know how to start processes, let's see what we can do with running processes.
```
ecmachine:/ guest$ (processes)
((-1 Terminal) (4 clock.app))
ecmachine:/ guest$ (peek 4)
(overlay (time (list 'h ': 'm ': 's)) -30 -30 'clock)
ecmachine:/ guest$ (kill 4)
(Process with PID 4 [clock.app] terminated)
```
There are three commands for dealing with processes, that are illustrated above:
- `(processes)` returns a list of running processes, with each process represented as a `(pid name)` pair
- `(peek pid)` shows the application code for the given process
- `(kill pid)` kills the given process and hides all overlays that were used by it
Note that the terminal itself is always represented as process -1, but cannot be killed. As we will see later, this means that something like `(map kill (map car (processes)))` won't work, while `(map kill (filter (λ (x) (> x -1)) (map car (processes))))` will.
### Process Performance
ECMAchine keeps track of the performance of every process (including the terminal) with a metric called _evals per second_. Every time a call to `lispEval` is made by a process, its eval count is incremented, and the evals/sec of a process is simply the amount of evals made divided by the number of seconds it has been running.
The `(performance pid)` function returns the evals/sec of the given process:
```
ecmachine:/ guest$ (start (path 'apps 'clock.app) 1000)
(Starting process at apps/clock.app with PID 4)
ecmachine:/ guest$ (processes)
((-1 Terminal) (4 clock.app))
ecmachine:/ guest$ (performance 4)
15.22
```
Of course, the performance of a process depends both on the application and on the refresh interval. If we set the clock to update every millisecond, we get:
```
ecmachine:/ guest$ (start (path 'apps 'clock.app) 1)
(Starting process at apps/clock.app with PID 5)
ecmachine:/ guest$ (performance 5)
3196.686
```
We will use the `performance` function later to write a simple task manager application.
Recipes
-----
So, now that we have all of these tools, what can we do with them? Here are some functions and processes that I've come up with. Many of them are included in ECMAchine as scripts, processes, or library functions.
### File System Recipes
#### Directory Cleanup
Let's start with file and folder manipulation. Here's one that was already mentioned above: a function to delete all of the elements of a directory.
```lisp
(define (clean dir)
(map rm
(map (lambda (x) (path dir x))
(ls dir))))
```
Why is the `(lambda (x) (path dir x))` mapping necessary? Let's say that we want to delete the contents of subfolder `/usr` and `(ls 'usr)` gives `(a b)`. We really want to execute `(rm (path 'usr 'a))` and `(rm (path 'usr 'b))`, rather than `(rm 'a)` and `(rm 'b)`. In other words, this mapping is used to preserve filepaths when traversing directory contents.
#### File/Folder Size
Now let's try a more complicated example: calculating the size of a file or directory.
Suppose that we're trying to find the size of `item`. If `item` is a file, then this is pretty easy - we can just read `item` and take its length. If `item` is a directory, then we need to take the sum of the sizes of its contents, using a similar technique to what we used above. Writing a function to sum a list is straightforward:
```lisp
(define (sum lst)
(if (null? lst)
0
(+ (car (lst)
(sum (cdr lst))))))
```
And now we're ready to write `size`:
```lisp
(define (size item)
(cond ((file? item) (length (read item)))
((dir? item)
(sum (map size
(map (lambda (x) (path item x))
(ls item)))))
(#t 0)))
```
Note the many similarities between `clean` and `size`: the existence of higher-order functions like `map` allows us to write different file manipulation functions in a similar style.
Now that we have `size` written, we can write a simple application that periodically calculates the size of the entire filesystem and displays it. Calculating the filesystem size is simply done with `(size '/)`. This application is saved under `/apps/memoryMonitor.app` and is loaded at startup:
```lisp
(overlay (list 'Filesystem 'size: (/ (size '/) 1000) 'KB) -30 30 'memMon)
```
#### File Search
Now let's try using a similar structure to write a `search` function that searches for a file with a given name recursively within a directory, returning the file's path if it is found and `#f` otherwise.
As a first step, we need to be able to obtain a filename from a file path - there's no primitive for this, but we can write a quick one-liner to do this: since paths are represented as `/dir1/dir2/file`, calling `split('/')` on a path (via `js-apply`) results in `(dir1 dir2 file)`, and we can take the `car` of the reverse (`reverse` is implemented in terms of `append` in `/startup/utility.lsp`) of this list to get the filename:
```lisp
(define (get-name path)
(car (reverse (js-apply 'split path '/))))
```
Now that we have this out of the way, suppose we're given a path and we're trying to find a file within that path. If the path is pointing to a file, we just need to compare names to see if we've found what we're looking for. If the path is pointing to a directory, it gets trickier. Let's take all of the contents of the directory and run the same search on them, then filter out all of the false results. If there is anything left, then we have found our file; if not, then the directory does not contain that file. Our final function:
```lisp
(define (search dir name)
(cond ((file? dir)
(if (= (get-name dir) name)
dir
#f))
((dir? dir)
(let ((results
(filter (lambda (x) (!= x #f))
(map (lambda (x) (search x name))
(map (lambda (x) (path dir x))
(ls dir))))))
(if (> (length results) 0)
(car results)
#f)))
(#t #f)))
```
Alternatively, we can search for both files and folders with just a slight modification:
```lisp
(define (search dir name)
(cond ((= (get-name dir) name) dir)
((dir? dir)
(let ((results
(filter (lambda (x) (!= x #f))
(map (lambda (x) (search x name))
(map (lambda (x) (path dir x))
(ls dir))))))
(if (> (length results) 0)
(car results)
#f)))
(#t #f)))
```
Let's see it in action!
```
ecmachine:/ guest$ (define (search dir name)
.. (cond ((file? dir)
.. (if (= (get-name dir) name)
.. dir
.. #f))
.. ((dir? dir)
.. (let ((results
.. (filter (lambda (x) (!= x #f))
.. (map (lambda (x) (search x name))
.. (map (lambda (x) (path dir x))
.. (ls dir))))))
.. (if (> (length results) 0)
.. (car results)
.. #f)))
.. (#t #f)))
ecmachine:/ guest$ (search '/ 'mapreduce.lsp)
/startup/mapreduce.lsp
ecmachine:/ guest$ (search 'apps 'mapreduce.lsp)
#f
```
What if we want to find all filenames that _contain_ a certain string, rather than just exact matches? We can write a `contains` function for strings using JavaScript's `String.indexOf()` method:
```lisp
(define (contains haystack needle)
(!= -1 (js-apply 'indexOf haystack needle)))
```
and now we can just replace `(= (get-name dir) name)` with `(contains (get-name dir) name)`.
We can even search file bodies rather than filenames, by replacing `(= (get-name dir) name)` with `(contains (read dir) name)`.
### Process Manipulation Recipes
#### Process Cleanup
Manipulating processes is not so different from manipulating files. For starters, how would we use the `kill` function to kill all running processes at once?
Note that `(processes)` returns a list of pid-name pairs:
```
ecmachine:/ guest$ (processes)
((-1 Terminal) (0 clock.app) (1 analogclock.app) (2 processmonitor.app) (3 memorymonitor.app))
```
Attempting to kill the Terminal process would give an error, so what we really want to do is map `kill` onto `(0 1 2 3)`. We can do that as follows:
```lisp
(define (kill-all)
(map kill
(filter (lambda (x) (>= x 0))
(map car
(processes)))))
```
Does it work?
```
ecmachine:/ guest$ (kill-all)
((Process with PID 0 [clock.app] terminated) (Process with PID 1 [analogclock.app] terminated) (Process with PID 2 [processmonitor.app] terminated) (Process with PID
3 [memorymonitor.app] terminated))
```
#### A Simple Task Manager
Let's say we want to constantly keep track of the performance of our running processes, so that we can tell if any particular application is hogging up our resources.
First things first, we'll have to get the performance of every process - we can do this by taking the above recipe and replacing `kill` with `performance`. Of course, just having a list of numbers with no context wouldn't be very helpful - it would be better to get a list of pairs of the form `(name performance)`. We can do that as follows:
```lisp
(map (lambda (proc) (list (cadr proc) (performance (car proc))))
(processes))
```
and if we want to sort the processes by performance, in descending order of evals/sec, we can use `sort`:
```lisp
(sort
(map (lambda (proc) (list (cadr proc) (performance (car proc))))
(processes))
(lambda (proc) (- (cadr proc))))
```
This gives us:
```
((processmonitor.app 538.13) (memorymonitor.app 340.33) (analogclock.app 195.025) (clock.app 14.959) (Terminal 0.145))
```
We're almost there! Now let's say we want to put each pair on its own line - that is, we want to insert a newline between each pair. One way to do this is to write an `intersperse` function that inserts a given item between every pair of elements in a list:
```lisp
(define (intersperse lst elt)
(if (= (length lst) 1)
x
(cons (car x)
(cons y (intersperse (cdr x) y)))))
```
Now that we have these pieces, we can put it all together to make an application that sorts the running processes by performance and displays the results as an overlay:
```lisp
(let ((header (list 'Processes '{evals/sec}))
(perfInfo (sort
(map (lambda (proc) (list (cadr proc) (performance (car proc))))
(processes))
(lambda (proc) (- (cadr proc))))))
(overlay (intersperse (cons header perfInfo) (newline)) -30 70 'procMon))
```
This application is saved in `/apps/processmonitor.app` and can be run at 1-second intervals with:
```lisp
(start (path 'apps 'processmonitor.app) 1000)
```
### Miscellaneous Recipes
#### Analog Clock
Who says that you can't have fancy graphics in a text-based environment? By setting up multiple overlays and performing some trigonometry, we can build an analog clock widget, just like the kind you can get in your fancy operating system of choice:
```lisp
(let* ((center-x -120) (center-y -160)
(pi 3.141592653589793)
(s-angle (* (/ pi 30) (+ (car (time '(s))) 15)))
(m-angle (* (/ pi 30) (+ (car (time '(m))) 15 (/ (+ (car (time '(s))) 15) 60))))
(h-angle (* (/ pi 6) (+ (car (time '(h))) 15 (/ (+ (car (time '(m))) 15) 60))))
(x-pos-s (- center-x (* 90 (math 'cos s-angle))))
(y-pos-s (- center-y (* 90 (math 'sin s-angle))))
(x-pos-m (- center-x (* 60 (math 'cos m-angle))))
(y-pos-m (- center-y (* 60 (math 'sin m-angle))))
(x-pos-h (- center-x (* 30 (math 'cos h-angle))))
(y-pos-h (- center-y (* 30 (math 'sin h-angle)))))
(begin
(overlay '(O) center-x center-y 'analogclockcenter)
(overlay '(h) x-pos-h y-pos-h 'analogclockhours)
(overlay '(m) x-pos-m y-pos-m 'analogclockminutes)
(overlay '(s) x-pos-s y-pos-s 'analogclockseconds)))
```
(`let*` is a special binding construct that evaluates each binding in an environment containing the previous bindings. We can't use the regular `let` construct here because some of the bindings depend on each other.)
This application is saved in `/apps/analogclock.app` and can be run with:
```lisp
(start (path 'apps 'analogclock.app) 1000)
```
#### Other Ideas
Some other ideas for applications that I've been playing around with but haven't yet finalized code for include:
- Automated backup
- Simple anti-virus
- Notes manager
ed, a Simple Text Editor
-----
We've seen some simple examples of recipes using ECMAchine's system methods, but can we build a more sophisticated application? For example, let's try taking a look at how to build a simple text editor, which I am going to give the remarkably unoriginal name of **ed**. It's not very fancy, but it does its job and can respond to 15 different commands. And, not counting a few simple helper functions, the whole editor takes up less than 100 lines of Lisp.
_Note: `ed` is not currently defined in ECMAchine by default. To use it, first copy all of the code at the [end of this section](#puttingitalltogether) into the console and run it._
### Introducing ed
Before we get into implementation details, let's first try running **ed**. You can start the program with the `(ed)` function, at which point an overlay appears in the top-left corner of the screen displaying the current contents of the text buffer. Operations are performed by running commands of the form `(ed 'command <params>)`, and editing is line-by-line: a cursor denotes the current line and lines can be created, modified, or deleted.
For example:
```
ecmachine:/ guest$ (ed 'new)
ecmachine:/ guest$ (ed 'write "line1")
ecmachine:/ guest$ (ed 'insert-after "line2")
ecmachine:/ guest$ (ed 'insert-after "line3")
ecmachine:/ guest$ (ed 'up) % moves the cursor up
ecmachine:/ guest$ (ed 'delete)
ecmachine:/ guest$ (ed 'write " :-}")
ecmachine:/ guest$ (ed 'save-as 'test.txt)
(Saved file /test.txt)
ecmachine:/ guest$ (read 'test.txt)
line1
line3 :-}
```
ed supports the following commands:
- **new**: `(ed 'new)` opens a new blank file
- **open**: `(ed 'open path)` opens the file at _path_
- **save**: `(ed 'save)` saves the contents of the text buffer to the current file location (if any)
- **save-as**: `(ed 'save-as path)` saves the contents of the text buffer to _path_
- **exit**: `(ed 'exit)` exits ed
- **up**: `(ed 'up)` moves the cursor up one line
- **down**: `(ed 'down)` moves the cursor down one line
- **write**: `(ed 'write text)` appends `text` to the current line
- **edit**: `(ed 'edit text)` replaces the current line with `text`
- **delete**: `(ed 'delete)` deletes the current line
- **insert-before**: `(ed 'insert-before text)` inserts `text` into a new line before the current line
- **insert-after**: `(ed 'insert-after text)` inserts `text`
- **map**: `(ed 'map func)` maps every line of the text buffer with `func` (that is, every line `L` is replace with `func(L)`
- **run**: `(ed 'run)` runs the contents of the text buffer as Lisp in the global environment
### Design Overview
The two big questions that needed to be answered when designing ed were:
1. Is ed going to be an application or a function?
2. How is the text buffer stored?
3. How is the text buffer displayed?
While implementing ed as an application would have allowed it to run persistently, a text editor doesn't really need to perform any operations on its own, because everything that it does (e.g. load/save files, modify text, move the cursor) is done directly in response to user input. As such, I decided that it would be more straightforward to simply implement ed as a function `(ed)`, that takes a string literal representing the operation to perform as an argument, as well as an optional second argument for some operations.
*ed* stores the current text buffer as a newline-delimited string in the `ed.contents` variable. Storing the text as a string rather than a list of lines makes it easy to save and load files, since text files are stored as strings as well. On the other hand, line-by-line manipulation requires a linked-list structure to work, so `ed.contents` needs to be converted into a list of lines before every edit command and then be converted back into a string. Fortunately, this isn't too hard to implement, as will be shown in the next section.
ed displays the current text buffer in an top-left-aligned overlay containing the filename and then a list of lines. The current position of the cursor is displayed as a caret **'> '** before the line it's on.
The current filename is stored in `ed.filename` and the current line number is stored in `ed.cursor`.
### Helper Functions
Now we can get started on writing code, but before we can start ed itself, we need to make a few more general helper functions.
First of, we need to be able to switch the contents of text files between a single newline-separated string (for storage and display) and a list of lines (for line-by-line manipulation). This is pretty easy to accomplish using JavaScript's `String.split` and `Array.join` methods:
```scheme
(define (join-lines arr)
(js-apply 'join arr "\n"))
(define (split-lines str)
(js-apply 'split str "\n"))
```
Storing files as lists of strings makes it easy to create, modify, and delete lines, using generic methods for modifying/creating/deleting the nth element of a list:
```scheme
(define (map-at-index func lst i)
(if (= i 0)
(cons (func (car lst)) (cdr lst))
(cons (car lst) (map-at-index func (cdr lst) (- i 1)))))
(define (insert-at-index elt lst i)
(if (= i 0)
(cons elt lst)
(cons (car lst) (insert-at-index elt (cdr lst) (- i 1)))))
(define (remove-at-index lst i)
(if (= i 0)
(cdr lst)
(cons (car lst) (remove-at-index (cdr lst) (- i 1)))))
```
Finally, for some tasks (such as rendering the cursor), it's helpful to have a more powerful `map` function whose functions take both the list element and its index in the list as arguments. Let's call it `mapi`. Unfortunately, I can't think of any elegant way to do this that doesn't involve a separate helper function:
```scheme
(define (mapi proc items)
(mapi-helper proc items 0))
(define (mapi-helper proc items i)
(if (null? items)
nil
(cons (proc (car items) i)
(mapi-helper proc (cdr items) (+ i 1)))))
```
### The Skeleton of ed
The `(ed)` function has a fairly simple overall structure. It can take between 0 and 2 parameters (via the construct `(define (ed . args) ...)` ). Calling `(ed)` without any parameters defaults to `(ed 'refresh)`, which renders the contents of the text buffer onto the screen. If `(> 0 (length args))`, then the arguments are separated into `func` and `params` (which is either a list of a single element or an empty list) - some commands take a parameter and some don't. The rest of `(ed)` is taken up by a single `cond` block that selects the appropriate command:
```scheme
(define (ed . args)
(if (= 0 (length args))
(ed 'refresh)
(let ((func (car args))
(params (cdr args)))
(cond ((= func <command>) <implementation>)
((= func <command>) <implementation>)
. . .
))))
```
### Displaying the Editor Contents
Given that the contents of current text buffer are stored as a string in `ed.contents`, how would we go about displaying them?
Well, we could just display them directly:
```scheme
(overlay ed.contents 30 30 'ed)
```
Ah, that wasn't too bad. How about if we want to indicate the position of the cursor at the line whose number is `ed.cursor`? Let's do this by prefixing the current line with "`>`" and prefixing all other lines with "`. `". To do this, we'd have to (1) convert the string in `ed.contents` into a list of lines, (2) transform each line based on whether its number equals `ed.cursor`, and (3) convert back to a newline-delimited string for display:
```scheme
(join-lines
(mapi (lambda (x i)
(if (= i ed.cursor)
(+ "> " x)
(+ ". " x)))
(split-lines ed.contents)))
```
We're almost there! One last thing that'd be nice would be displaying the name of the current file (or `"New File"` if the file has not been loaded/saved yet). And while we're add it, let's add a nice title bar of sorts:
```scheme
((= func 'refresh)
(let ((display-contents
(join-lines
(append
(list "::: ECMAchine Text Editor (v0.1) :::"
(+ "== " (if (= ed.filename "") "New File" ed.filename) " =="))
(mapi (lambda (x i)
(if (= i ed.cursor)
(+ "> " x)
(+ ". " x)))
(split-lines ed.contents))))))
(overlay display-contents 30 30 'ed)))
```
Not too shabby. And since there's no process running in the background for **ed**, exiting is just a matter of clearing this overlay:
```scheme
((= func 'exit) (clear-overlay 'ed))
```
Most of the commands that follow use Scheme's `begin` construct, which executes a sequence of functions in order. We'll need to use `begin` because a lot of the time we'll want to modify the contents of **ed** and then refresh the display. In other words, the general template for most editing commands will be:
```scheme
(begin
% do something
(ed 'refresh))
```
Ok, let's begin defining commands!
### File Commands
**ed** has four commands that deal directly with files: `new`, `open`, `save`, and `save-as`.
When `(ed 'new)` is executed, the three variables used by **ed** (`ed.filename`, `ed.contents`, and `ed.cursor`) are all reset:
```scheme
((= func 'new)
(begin
(set! ed.filename "")
(set! ed.contents "")
(set! ed.cursor 0)
(ed 'refresh)))
```
`(ed 'open <file>)` sets `ed.filename` and `ed.contents` to be those of _<file>_ (where _<file>_ is a valid path):
```scheme
((or (= func 'open) (= func 'load))
(begin
(set! ed.filename (car params))
(set! ed.contents (read ed.filename))
(ed 'refresh)))
```
`(ed 'save)` and `(ed 'save-as <file>)` save `ed.contents` to either the current file (if any) or a specified _<file>_:
```scheme
((= func 'save)
(if (= ed.filename "")
'(No filename chosen - use the 'save-as command instead)
(save ed.filename ed.contents)))
((= func 'save-as)
(begin
(set! ed.filename (car params))
(ed 'refresh)
(save ed.filename ed.contents)))
```
### Edit Commands
The `up` and `down` command generally move the cursor by decrementing or incrementing, respectively, the value stored in `ed.cursor`. Special care must be taken to make sure that `ed.cursor` stays within the bounds of the file:
```scheme
((= func 'up)
(begin
(set! ed.cursor (math 'max (list 0 (- ed.cursor 1))))
(ed 'refresh)))
((= func 'down)
(begin
(set! ed.cursor (math 'min (list (+ ed.cursor 1) (- (length (split-lines ed.contents)) 1))))
(ed 'refresh)))
```
To append text to the current line for the `write` command: we can (1) split `ed.contents` into a list of lines, (2) find the current line and append the given text to it via `map-at-index`, and (3) join the resulting list back into a newline-delimited string:
```scheme
((= func 'write)
(begin
(set! ed.contents (join-lines (map-at-index (lambda (str) (+ str (car params))) (split-lines ed.contents) ed.cursor)))
(ed 'refresh)))
```
`edit` functions almost identically to `write`, except that it replaces the current line outright rather than appending to it, by mapping `(λ (str) (car params))` rather than `(λ (str) (+ str (car params)))`:
```scheme
((= func 'edit)
(begin
(set! ed.contents (join-lines (map-at-index (lambda (str) (car params)) (split-lines ed.contents) ed.cursor)))
(ed 'refresh)))
```
To remove the current line for the `delete` command, we can (1) split `ed.contents` into a list of lines, (2) find the current line and remove it via `remove-at-index`, and (3) join the resulting list back into a newline-delimited string:
```scheme
((= func 'delete)
(begin
(set! ed.contents (join-lines (remove-at-index (split-lines ed.contents) ed.cursor)))
(ed 'down)))
```
To insert new lines before or after the cursor, we can (1) split `ed.contents` into a list of lines, (2) find either the current line (for `insert-before`) or the following line (for `insert-after`) and insert a new line at its index via `insert-at-index`, and (3) join the resulting list back into a newline-delimited string:
```scheme
((= func 'insert-before)
(begin
(set! ed.contents (join-lines (insert-at-index (car params) (split-lines ed.contents) ed.cursor) ))
(ed 'refresh)))
((= func 'insert-after)
(begin
(set! ed.contents (join-lines (insert-at-index (car params) (split-lines ed.contents) (+ ed.cursor 1))))
(ed 'down)))
```
### Cool Things
Now that we can perform all of the basic commands expected of a simple file editor, let's try our hand at some features that are a little more interesting.
#### Map
Sometimes we want to apply an arbitrary function to every line in a file.
To this end, `map` takes an argument of the form `(λ (x i) ...)`, where `x` represents the contents of a given line and `i` its line number, and maps this function onto every line in the text buffer:
```scheme
((= func 'map)
(begin
(set! ed.contents (join-lines (mapi (car params) (split-lines ed.contents))))
(ed 'refresh)))
```
When can this come in handy? Well, let's say that we're reading a file and want to be able to see line numbers for it. This can be accomplished very easily using `(ed 'map)`, as can be seen below. (Note, however, that this edits the contents of the text buffer directly, so make sure not to save the file after using this!)