Java | Scheme | |
---|---|---|
if ( x.val() > 0 ) {<br> z = f(a * x.val() + b); <br>}
| (if (> (val x) 0)<br> (set! z (f (+ (* a (val x)) b))))
|
set!
". Only brackets are special characters. A list, such as (set! xy)
, with a special keyword at the beginning is called a special form in Scheme; The beauty of the language is that we need only 6 special forms, plus 3 syntactic constructions - variables, constants and procedure calls:
The form | Syntax | Semantics and Example |
---|---|---|
variable reference
| var
| A symbol is interpreted as a variable name, its value is a variable value.
Example: x
|
constant literal
| number
| The number means itself.
Examples: 12 or -3.45e+6
|
quote
| (quote exp )
| Returns exp literally without evaluating it.
Example:
|
conditional
| (if test conseq alt )
| Calculate test ; if true, calculate and return conseq ; otherwise calculate and return alt .
Example: (if (< 10 20) (+ 1 1) (+ 3 3)) ⇒ 2
|
assignment
| (set! var exp ) | Calculate exp and assign the value to the variable var , which should be previously defined (using define or as a procedure parameter).
Example: (set! x2 (* xx))
|
definition
| (define var exp )
| Define a new variable in the nested environment itself and give it the value of the expression exp .
Examples: (define r 3) or (define square (lambda (x) (* xx))) .
|
function
| (lambda ( var ... ) exp )
| Create a function with a parameter (s) with the names var ... and an expression in the body.
Example: (lambda ( r ) (* 3.141592653 (* rr)))
|
sequencing | (begin exp ... )
| Calculate each expression from left to right and return the last value.
Example: (begin (set! x 1) (set! x (+ x 1)) (* x 2)) ⇒ 4
|
procedure call
| ( proc exp ... )
| If proc is not if, set!, define, lambda, begin, or quote then it is considered as a procedure. It is calculated according to the same rules as defined here. All expressions are evaluated, and then the function is called with a list of expressions in the argument.
Example: (square 12) ⇒ 144
|
x
or square
; number - must be an integer or floating point number, while the remaining italicized words can be any expression. The notation exp ... means zero or more reps of exp .
parse
function.
eval
function (note, it hides the built-in Python function).
parse
and eval
handle a short program:
>> program = "(begin (define r 3) (* 3.141592653 (* r r)))"
>>> parse ( program )
[ ' begin ', [ ' define ', 'r', 3 ] , [ ' * ', 3.141592653 , [ ' * ', 'r', 'r' ] ] ]
>>> eval ( parse ( program ) )
28.274333877
eval
eval
nothing else is needed:
def eval ( x, env = global_env ) :
"Evaluate an expression in an environment."
if isa ( x, Symbol ) : # variable reference
return env. find ( x ) [ x ]
elif not isa ( x, list ) : # constant literal
return x
elif x [ 0 ] == 'quote' : # (quote exp)
( _, exp ) = x
return exp
elif x [ 0 ] == 'if' : # (if test conseq alt)
( _, test , conseq, alt ) = x
return eval ( ( conseq if eval ( test , env ) else alt ) , env )
elif x [ 0 ] == 'set!' : # (set! var exp)
( _, var, exp ) = x
env. find ( var ) [ var ] = eval ( exp, env )
elif x [ 0 ] == 'define' : # (define var exp)
( _, var, exp ) = x
env [ var ] = eval ( exp, env )
elif x [ 0 ] == 'lambda' : # (lambda (var *) exp)
( _, vars , exp ) = x
return lambda * args: eval ( exp, env ( vars , args, env ) )
elif x [ 0 ] == 'begin' : # (begin exp *)
for exp in x [ 1 : ] :
val = eval ( exp, env )
return val
else : # (proc exp *)
exps = [ eval ( exp, env ) for exp in x ]
proc = exps. pop ( 0 )
return proc ( * exps )
isa = isinstance
Symbol = str
define
or ( lambda
expression).
lis.py>
means that we interact with the Lisp interpreter, not Python):
lis.py > ( define area ( lambda ( r ) ( * 3.141592653 ( * r r ) ) ) )
lis.py > ( area 3 )
28.274333877
(lambda ( r ) (* 3.141592653 (* rr)))
, we fall into the elif x[0] == 'lambda'
branch in eval
, assign the three elements of the list x
( and signal an error if the length x
not 3). We create a new function that, when called, will compute the expression ['*', 3.141592653 ['*', 'r', 'r']]
in the environment formed by a bunch of formal parameters of the function (in this case, only one parameter r
) with arguments in the function call, as well as using the current environment for any variables that are not parameters (in the example, the variable *
). The value of this newly-minted function is then assigned as the value of the area
in global_env
.
(area 3)
? Because area is not a special form of characters, so this is a function call (the last else:
in eval
), and the entire list of expressions is executed at once. The area
calculation returns the function we just created; calculating 3
returns 3
. Then (according to the last line of eval
) the newly created function with the list of arguments [3]
called. This means exp
, which is ['*', 3.141592653 ['*', 'r', 'r']]
in an environment where r
is 3
and the external environment is global and therefore *
is a multiplication function.
Env
class:
class Env ( dict ) :
"An environment: a dict of {'var': val} pairs, with an outer Env."
def __init__ ( self , parms = ( ) , args = ( ) , outer = None ) :
self . update ( zip ( parms, args ) )
self . outer = outer
def find ( self , var ) :
"Find the innermost Env where the var appears."
return self if var in self else self self . outer . find ( var )
Env
class is a subclass of dict
, which means that it is using regular dictionary operations. In addition, there are two methods, the constructor __init__
and find
for finding the right environment for a variable. The key to understanding this class (as well as the reason that we do not use just dict
) is the concept of the external environment. Consider the following program:
a1
and call (a1 -20.00)
; These two lines represent the creation of a bank account with a balance of $ 100 and the subsequent withdrawal of $ 20. In the process of execution (a1 -20.00)
we calculate the expression highlighted in yellow. 3 variables are involved in this expression. The amt
variable can be found immediately in the most nested (green) environment. But balance
not defined here, we will search in the external environment relatively green, ie in blue. Finally, the variable +
not found in any of these environments; we need to take another step into the global (red) environment. This search process, from the internal environment to the external, is called lexical scoping. Procedure.find
finds the right environment according to the lexical scope.
+
and all other functions built into Scheme. We will not worry about the implementation of all of them, we will get a bunch of necessary by importing the math
module in Python and then explicitly adding 20 popular ones:
def add_globals ( env ) :
"Add some Scheme standard procedures to an environment."
import math , operator as op
env. update ( vars ( math ) ) # sin, sqrt, ...
env. update (
{ '+' : op. add , '-' : op. sub , '*' : op. mul , '/' : op. div , 'not' : op. not_ ,
'>' : op. gt , '<' : op. lt , '> =' : op. ge , '<=' : op. le , '=' : op. eq ,
'equal?' : op. eq , 'eq?' : op. is_ , 'length' : len , 'cons' : lambda x, y: [ x ] + y,
'car' : lambda x: x [ 0 ] , 'cdr' : lambda x: x [ 1 : ] , 'append' : op. add ,
'list' : lambda * x: list ( x ) , 'list?' : lambda x: isa ( x, list ) ,
'null?' : lambda x: x == [ ] , 'symbol?' : lambda x: isa ( x, Symbol ) } )
return env
global_env = add_globals ( Env ( ) )
read
and parse
parse
function. Parsing is traditionally divided into two parts: lexical analysis , during which the input string of characters is divided into a sequence of tokens , and syntactic analysis , during which the tokens are assembled into an internal representation. Lispy tokens are brackets, characters (such as set!
Or x
), and numbers. Here's how it works:
>>> program = "(set! x * 2 (* x 2))"
>>> tokenize ( program )
[ ' ( ', ' set! ', 'x * 2 ', ' ( ', ' * ', 'x', ' 2 ', ' ) ', ' ) ' ]
>>> parse ( program )
[ ' set! ',' x * 2 ', [ ' * ',' x ', 2 ] ]
str.split
. We simply add spaces around each bracket, and then get a list of tokens by calling str.split
.
(set! 1 2)
will be accepted by the syntactically correct program, and only at run time the interpreter will swear that set!
expects the first argument to be a character, not a number. In Java or Python, the equivalent assignment of 1 = 2
will be recognized as an error at compile time. On the other hand, Java and Python do not require detection at compile time of an error in the x/0
expression, therefore, as you can see, it is not always strictly defined when errors should be recognized. Lispy implements parse
using read
, a function that reads any expression (number, character, or nested list).
read
function works by passing read_from
tokens obtained after lexical analysis. Having a list of tokens, we look at the first token; if this is a syntax error. If this is '('
, then we start building a list of expressions until we get to the corresponding ')'
. Everything else must be characters or numbers, which are full expressions in their own right. The final trick is knowing that '2'
represents an integer, '2.0'
is a floating point number, and x
represents a character. We allow Python to make these distinctions: for each “non-bracket” token, we first try to interpret it as an integer, and then as a floating point number, and finally as a symbol. Following these instructions will get:
def read ( s ) :
"Read a Scheme expression from a string."
return read_from ( tokenize ( s ) )
parse = read
def tokenize ( s ) :
"Convert a string into a list of tokens."
return s. replace ( '(' , '(' ) . replace ( ')' , ')' ) . split ( )
def read_from ( tokens ) :
"Read an expression of a sequence of tokens."
if len ( tokens ) == 0 :
raise SyntaxError ( 'unexpected EOF while reading' )
token = tokens. pop ( 0 )
if '(' == token :
L = [ ]
while tokens [ 0 ] ! = ')' :
L. append ( read_from ( tokens ) )
tokens pop ( 0 ) # pop off ')'
return L
elif ')' == token :
raise SyntaxError ( 'unexpected)' )
else :
return atom ( token )
def atom ( token ) :
"Numbers become numbers; every other token is a symbol."
try : return int ( token )
except ValueError :
try : return float ( token )
except ValueError :
return Symbol ( token )
to_string
function in order to convert the expression back to a readable Lisp string, as well as the repl
function, which is needed for the read-eval-print loop in the form of an interactive interpreter:
def to_string ( exp ) :
"Convert a Python object back into a Lisp-readable string."
return '(' + '' . join ( map ( to_string, exp ) ) + ')' if isa ( exp, list ) else str ( exp )
def repl ( prompt = 'lis.py>' ) :
"A prompt-read-eval-print loop."
while true :
val = eval ( parse ( raw_input ( prompt ) ) )
if val is not None : print to_string ( val )
>>> repl ( )
lis.py > ( define area ( lambda ( r ) ( * 3.141592653 ( * r r ) ) ) )
lis.py > ( area 3 )
28.274333877
lis.py > ( define fact ( lambda ( n ) ( if ( <= n 1 ) 1 ( * n ( fact ( - n 1 ) ) ) ) ) )
lis.py > ( fact 10 )
3628800
lis.py > ( fact 100 )
93326215443944152681699238856266700490715968264381621468592963895217599322991
56089414639761565182862536979208272237582511852109168640000000000000000000000
lis.py > ( area ( fact 10 ) )
4.1369087198e + 13
lis.py > ( define first car )
lis.py > ( define rest cdr )
lis.py > ( define count ( lambda ( item L ) ( if L ( + ( equal? item ( first L ) ) ( count item ( rest L ) ) ) 0 ) ) )
lis.py > ( count 0 ( list 0 1 2 3 0 0 ) )
3
lis.py > ( count ( quote the ) ( quote ( the more the bigger the better the better ) ) )
four
Procedure
class and use the Python lambda
instead. The smallest Java version of my Scheme ( Jscheme ) consisted of 1664 lines and 57K source code. Jsceheme was originally called SILK (Scheme in Fifty Kilobytes, Scheme is 50 kilobytes), but I only met this limit in relation to the resulting bytecode. I think it satisfies Alan Kay’s 1972 requirement that you could define “the most powerful language in the world” in the “code page” .
bash $ grep "^ \ s * [^ # \ s]" lis.py | wc
90 398 3423
(fact 100)
in 0.004 seconds. For me, this is quite fast (although much slower than most other ways to calculate it).
cond
, derived from if
, or let
, derived from lambda
), and point list notation.
set-car!
And set-cdr!
we cannot implement set-cdr!
Completely using Python lists) .
Source: https://habr.com/ru/post/115206/