summaryrefslogtreecommitdiff
path: root/artima/scheme/scheme16.ss
blob: 646314dbfd292b90dd0274445bfa2ff5b184d7b1 (plain)
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
#|
Multiple values (and opt-lambda)
================================================

This episode is a direct continuation of latest issue: it gives
example of use of the destructuring bind form let+ introduced there. I
also discuss multiple values, unary functions and functions with
optional arguments.

list destructuring versus let-values
---------------------------------------------------------------------------

There is a feature of Scheme that I never liked, i.e. the fact that
functions (more in general continuations) can return multiple values.
Multiple values are a relatively late addition to Scheme - they entered in the
standard with the R5RS report - and there has always been some opposition
against them (see for instance `this old post`_ by Jeffrey Susskind).
I personally see multiple values as a wart of Scheme, a useless complication
motivated by premature optimization concerns.

It is possible to define functions
returning multiple values as follows::

 > (define (return-three-values)
     (values 1 2 3))
 > (return-three-values)
 1
 2
 3

In order to receive the values a
special syntax is needed, and you cannot do things like the following::

 > (apply + (return-three-values))
 Unhandled exception 
  Condition components: 
    1. &assertion 
    2. &who: apply 
    3. &message: "incorrect number of values returned to single value context" 
    4. &irritants: ((1 2 3))

Instead, you are forced to use ``let-values`` or other constructs which
are able to accept values::

 > (let-values (((x y z) (return-three-values))) (+ x y z))
 6

In this series I will never use functions returning multiple values,
except the ones in the Scheme standard library (this is why I am
forced to talk about ``let-values``). Instead of using multiple
values, I will return a list of values and I will destructure it
with ``let+``. For instance, I will write

::

 > (let+ ((a b) (list 1 2)) (cons a b))
 (1 . 2)

instead of 

::

 > (let-values (((a b) (values 1 2))) (cons a b))
 (1 . 2)

``let+`` is more elegant and more general than ``let-values``:
everything ``let-values`` can do, ``let+`` can do too.  ``let+`` can
even faster - in some implementations and in some cases.
Here is a benchmark in Ikarus Scheme::

 running stats for (repeat 10000000 (let-values (((x y z) (values 1 2 3))) 'dummy)):
     no collections
     276 ms elapsed cpu time, including 0 ms collecting
     277 ms elapsed real time, including 0 ms collecting
     0 bytes allocated

 running stats for (repeat 10000000 (let+ ((x y z) (list 1 2 3)) 'dummy)):
     58 collections
     211 ms elapsed cpu time, including 42 ms collecting
     213 ms elapsed real time, including 43 ms collecting
     240016384 bytes allocated

As you see, ``let+`` takes only 211 ms to unpack a list of three elements
ten million times; ``let-values`` would take 276 ms instead.
On the other hand, ``let+`` involves garbage collection
(in our example 24 bytes are allocate per cycle, and thats means 240
million of bytes) and depending on the situations and the implementation
this may cause a serious slowdown. You may find much better benchmarks
than mine in `this thread`_ on comp.lang.scheme and you will see that
you can get any kind of results. ``let-values`` seems to be slow in
Ikarus and in Ypsilon with the default optimization options; it can be
faster in other implementations, or in the same implementations with
different options or in different releases.

However, those are implementation
details.
The important thing in my view is the conceptual relevance of a language
construct. Conceptually I think the introduction of multiple values
in Scheme was a mistake, since it added nothing that could not be done
better with containers. I think functions should *always* return
a single value, possibly a composite one (a list, a vector, or anything
else). Actually, I am even more radical than that and I think that functions
should take a *single value*, as in SML and Haskell.

.. _this old post: http://groups.google.com/group/comp.lang.scheme/msg/7335da47820deff4?hl=en
.. _this thread: http://groups.google.com/group/comp.lang.scheme/browse_frm/thread/ba8873b2f955af67#

Variadic functions from unary functions
--------------------------------------------------------------------

If you have a minimalistic mindset (as I have) you will recognize that
multiple argument functions are useless since they can be implemented
as unary functions performing destructuring.
Here is a simple implementation of the idea:

$$FN

Here are a few examples of usage::

 > (define/fn (double x) (* 2 x))
 > (double '(1))
 2

 > (define/fn (sum . vals) (apply + vals))
 > (sum '(1 2 3))
 6

 > (define/fn (sum-2D (x1 y1) (x2 y2)) (list (+ x1 x2) (+ y1 y2)))
 > (sum-2D '((1 2)(3 4)))
 (4 6)

All the functions defined via ``define/fn`` take a single argument, a list,
which is then destructured according to the declared structure.
``double`` expects a list with a single element named ``x``; ``sum`` expects
a list with a variable number of elements ``val``; ``sum-2D`` expects
a two-element lists made of two-element lists named ``(x1 y1)`` and
``(x2 y2)`` respectively. You can easily imagine more complex examples
with deeply nested lists.

.. image:: unpacking.png

It is interesting to notice that Python has the
list destructuring/tuple unpacking functionality built-in:

>>> def sum2D((x1, y1), (x2, y2)):
...     return x1 + x2, y1 + y2
... 
>>> sum2D((1,2),(3,4))
(4, 6)

This is valid Python code in all versions of Python before Python 3.0.
However, in Python 3X this functionality has been removed for lack of use
(*sic*).

The advantage of unary functions is that they are easier to compose,
and many functional patterns (including currying described in
`episode #14`_) becomes possible. However, Scheme is not ML or Haskell, so
let us accept functions with multiple arguments and let us take advantage
of them to implement optional arguments. This is the subject of the
next paragraph.

.. _episode #14: http://www.artima.com/weblogs/viewpost.jsp?thread=249198

Further examples of destructuring: opt-lambda
---------------------------------------------------------------------

A weekness of standard Scheme is the lack of function with default
arguments and keyword arguments. In practice, this is a minor weakness
since there many libraries implementing the functionality, although in
different ways, as usual. I recommend you to look at SRFI-88_ and
SRFI-89_ for more context. Here I will implement equivalent
functionality from scratch,
as yet another exercise to show the power of
``let+``. Let me start from an example, to make clear the intended
functionality. Let me define a function ``f`` with optional arguments
as follows:

$$F

Here ``x`` and ``y`` are required arguments, ``u`` and ``v``
are optional arguments and ``rest`` are variadic arguments.
If you do not provide an optional argument, its default value is
be used instead, and ``f`` behaves as follows::

 > (f 'a 'b 'c 'd 'e 'f)
 Required: (a b) Optional: (c d) Other: (e f)
 > (f 'a 'b 'c)
 Required: (a b) Optional: (c 2) Other: ()
 > (f 'a 'b)
 Required: (a b) Optional: (1 2) Other: ()
 > (f 'a)
 Unhandled exception
  Condition components:
    1. &assertion
    2. &who: apply
    3. &message: "incorrect number of arguments"
    4. &irritants: (#<procedure f> 1)

It is clear that in order to implement the functionality the trick
is to override the defaults of the optional argument with the passed
arguments, if any. To this aim we will need the following helper
function:

$$OVERRIDE-WITH

(we introduced the ``list-tail`` function in `episode #13`_).
At this point it is easy to define an ``opt-lambda`` macro doing the
job:

$$OPT-LAMBDA

``define/opt`` is just sugar over ``opt-lambda``:

$$DEFINE/OPT

I should notice that strictly speaking you do not need a full
restructuring form to implement ``opt-lambda``: since ``override-with-args``
returns a flat list, a form which is able to destructure flat list is
enough. You could implement it easily enough:

$$LET-

However ``let+`` subsumes the functionality of ``let-`` and I do not
see the point of introducing yet another binding form, except for sake
of the exercise. Strangely enough, ``let-`` looks even slower than ``let+``
in Ikarus::

 running stats for (repeat 10000000 (let- (x y z) (list 1 2 3) 'dummy)):
    58 collections
    324 ms elapsed cpu time, including 16 ms collecting
    324 ms elapsed real time, including 22 ms collecting
    240004096 bytes allocated

But please don't trust benchmarks! ;)

.. _SRFI-88: http://srfi.schemers.org/srfi-88/srfi-88.html
.. _SRFI-89: http://srfi.schemers.org/srfi-89/srfi-89.html
.. _episode #13: http://www.artima.com/weblogs/viewpost.jsp?thread=248953

|#


(import (rnrs) (sweet-macros) (aps list-utils) (aps easy-test)
        (repeat-macro) (ikarus))

;;FN
(def-syntax (fn (arg ... . rest) body body* ...)
  #'(lambda (x)
      (let+ ((arg ... . rest) x)
        (begin body body* ...))))

(def-syntax (define/fn (name arg ... . rest) body body* ...)
  #'(define name (fn (arg ... . rest) body body* ...)))
;;END

;OVERRIDE-WITH
;;(override-with '(a b) '(1 2 3)) => '(a b 3)
(define (override-with winner loser)
  (let ((w (length winner)) (l (length loser)))
    (if (>= w l)
        winner ; winner completely overrides loser
        (append winner (list-tail loser w)))))
;END

;OPT-LAMBDA
(def-syntax opt-lambda
  (syntax-match (opt)
    (sub (opt-lambda (r1 ... (opt (o1 d1) ...) . rest) body1 body2 ...)
    #'(lambda (r1 ... . args)
        (let+ ((o1 ... . rest) (override-with args (list d1 ...)))
              (begin body1 body2 ...))))))
;END

;DEFINE/OPT
(def-syntax (define/opt (name . args) body1 body2 ...)
  #'(define name (opt-lambda args body1 body2 ...)))
;END


;;F
   (define/opt (f x y (opt (u 1) (v 2)) . rest)
      (printf "Required: ~a Optional: ~a Other: ~a\n"
         (list x y) (list u v) rest))
;;END


;;LET-
(def-syntax (let- (name ... . rest) lst expr)
  #'(apply (lambda (name ... . rest) expr) lst))
;;END

(time (repeat 10000000 (let-values (((x y z) (values 1 2 3))) 'dummy)))
(time (repeat 10000000 (let+ ((x y z) (list 1 2 3)) 'dummy)))
(time (repeat 10000000 (let- (x y z) (list 1 2 3) 'dummy)))