Если единственный инструмент, которым вы располагаете, это молоток, то множество различных предметов покажутся вам гвоздями.
Марк Твен


Часть 1 Введение в Scheme
Часть 2 Углубление в Scheme
Часть 3 Практика IronScheme

Знакомимся ближе


Настало время изучить основные конструкции языка Scheme. Самый лучший способ научиться использовать новый язык это начать на нем писать. Начнем постепенное погружение с разбора самых базовых элементов языка.

Предлагаю запустить интерпретатор IronScheme в REPL режиме и вводить приведенные ниже команды.

Однострочный комментарий начинается с точки с запятой и действует до конца строки:
; эта строка будет игнорирована интерпретатором

Программа на Scheme состоит из списков заключенных в круглые скобки и разделенных пробелом(s-выражение). Вызов функции записывается как (f x y z …), где f имя функции, а x, y, z, … операнды.
(+ 2 2)  ;  => 4

Здесь мы выполнили операцию сложения двух чисел.

Выражения могут быть вложенными:
(+ 2 (+ 1 1))  ; => 4

Таким образом, выражения могут состоять из атомов или других выражений. В приведенном выше примере числа «1» и «2» атомы, а «(+ 2 (+ 1 1))» и «(+ 1 1)» выражения.

Примитивы


Числа:
9999999999999999999999 ; integers
#b111                  ; binary => 7
#o111                  ; octal => 73
#x111                  ; hexadecimal => 273
3.14                   ; reals
6.02e+23
1/2                    ; rationals
1+2i                   ; complex numbers

Чтобы создать список каких-либо значений, например из тех же примитивов, следует использовать функцию list, аргументы которой соберутся в список. Можно поступить другим способом, подавить вычисление. Подавление достигается функцией quote или ее сахарным аналогом одиночной кавычкой перед списком “ ’ ”. Важно помнить, что list не подавляет вычисления.
(list 1 2 3 4 5 (+ 1 2 3))    ; => (1 2 3 4 5 6)
(quote (1 2 3 4 5 (+ 1 2 3))) ; => (1 2 3 4 5 (+ 1 2 3))
'(1 2 3 4 5 (+ 1 2 3))        ; => (1 2 3 4 5 (+ 1 2 3))

Некоторые арифметические операции


(+ 1 1)              ; => 2
(- 8 1)              ; => 7
(* 10 2)             ; => 20
(expt 2 3)           ; => 8
(quotient 5 2)       ; => 2
(remainder 5 2)      ; => 1
(/ 35 5)             ; => 7
(/ 1 3)              ; => 1/3
(exact->inexact 1/3) ; => 0.3333333333333333
(+ 1+2i  2-3i)       ; => 3-1i


Булева алгебра


Для обозначения истины имеется примитив “#t”, ложь обозначается как “#f”, кроме того все другие значения отличные от “#f” трактуются как истина:
(not #t)   ; => #f
(and 0 #f) ; => #f
(or #f 0)  ; => 0


Символы, строки


Согласно стандарту RnRs, символы можно представить в коде двумя способами, символом который обозначает самого себя или кодом символа:
#\A     ; => #\A
#\x03BB ; => #\?

Строки являются массивами символов фиксированной длинны и заключаются в двойные кавычки:
"Hello, world!"

Кавычки внутри строки можно экранировать обратным слешем:
"Benjamin \"Bugsy\" Siegel" 

Для печати строки в стандартный вывод можно использовать функцию «display» принимающую в качестве аргумента строку:
(display "Some string")

Строки можно объединять:
(string-append "Hello " "world!") ; => "Hello world!"

Получить доступ к символу строки по индексу можно так:
(string-ref "Apple" 0) ; => #\A

Для форматирования строк удобно использовать функцию форматирования:
(format  "~a can be ~a" "strings" "formatted") ; => "strings can be formatted"

Чтобы результат форматирования не был потерян можно присвоить строку переменной:
(define str (format "~a can be ~a" "strings" "formatted"))


Переменные


Вы можете объявлять переменные, используя функцию «define», в которой первый аргумент имя функции, второй не обязательный аргумент значение которым переменная будет инициализирована. Имя переменной может содержать любые символы за исключением: ()[]{}”,’`;#/\
Например:
(define some-var 5)
some-var ; => 5

Переменные в Lisp без строгой типизации и могут указывать на атомы или функции.
Операция «set!» сохраняет значение в переменной, по сути, является аналогом оператора присвоения «=» из других языков. Не забываем сначала объявить переменную операцией «define»:
(define my-name "unknown")
my-name ; => "unknown"

(set! my-name "NalaGinrut")
my-name ; => " NalaGinrut "

Попытка доступа к прежде необъявленной переменной вызовет исключение.
Удобно объявлять сразу группу локальных переменных при помощи конструкции (let …).
(let 
  (
    (a "My") 
    (b "name") 
    (c "is") 
    (d "Bob")
  ) 
  (set! d "Sara")
  (format "~a ~a ~a ~a" a b c d)
)

; => My name is Sara


Функции


Для создания функции используется конструкция (lambda (a b c) body), где a, b, c аргументы, body последовательность команд.
(lambda () "Hello World")
(lambda (x) (+ x x)) 

Созданная выше функция не имеет имени, поэтому обратиться к ней нет возможности. Чтобы получить доступ к созданной функции ее можно присвоить переменной
(define hello-world (lambda () "Hello World"))
(hello-world) ; => "Hello World"

Или так:
(define hello-world)
(set! Hello-world (lambda () "Hello World"))
(hello-world) ; => "Hello World"

Обычно используется более удобная конструкция (define (function-name arg1 arg2) body)
(define (mul a b) (* a b))
(mul 2 3); => 6

Функция всегда возвращает последнее значение.
(define (last-string) “one” “two” “three”)
(last-string) ; => “three”


Управление потоком


Для ветвления в Scheme существуют, различные конструкции наиболее привычной, но не всегда самой удобной может показаться конструкция типа if-then-else
(if #t               ; условие
  "this is true"     ; если истина
  "this is false")   ;  ели ложь
; => "this is true"

Если в ветви необходимо выполнить несколько команд подряд, то их следует заключить в блок «begin»
(if (< 1 3) 
  (begin
    (display “one line”)
    (newline)
    (display “two line”)
    (- 1 3)
  )
)

Когда нужно проверить несколько условий удобна конструкция «cond»
(cond 
  ((> 2 2) "wrong!")
  ((< 2 2) "wrong again!")
  ((= 2 2) "ok")
  (else "wrong also")
)

Если код необходимо выполнить только в случае истины прекрасно подходит «when»
(when (< 1 3) “true”)

Организовать цикл можно двумя способами, рекурсией
(define (lp i)
  (when (< i 10)
    (display (format  "i=~a\n" i))
    (lp (+ 1 i))
  )
)
(lp 5) ; => i=5, i=6, ...

Или при помощи именованного «let»
(let loop ((i 0))                 ; definition
  (when (< i 10)                  ; condition
    (display (format "i=~a\n" i)) ; body
    (loop (+ 1 i))                ; next iteration
  )
) ; => i=0, i=1, ...


Макросы


Макросы Scheme довольно мощный инструмент, который позволяет расширять синтаксис языка, создавая новые конструкции. Однако не следует слишком увлекаться и применять макросы только там, где это действительно необходимо. Определение макроса начинается с команды «define-syntax»
(define-syntax macro
  (syntax-rules (<keywords>)
    ((<pattern>) <template>)
    ...
    ((<pattern>) <template>)
  )
)

<keywords> — Ключевые слова, которые можно будет использовать в описании шаблона. Например, можно написать макрос для конструкции «(forech (item in items) …)», в данном случае ключевым словом будет «in», которое обязательно должно присутствовать.

<pattern> — Шаблон, описывающий, что на входе у макроса.

<template> — Шаблон, описывающий, во что должен быть трансформирован В макросе многоточие «…» означает, что тело может содержать одну или более форм.

Рассмотрим применение макросов для создания циклов while и for.
(define-syntax while
  (syntax-rules ()
    ((while condition body ...)
      (let loop ()
        (when condition
          body ...
          (loop)
        )
      )
    )
  )
)

проверим созданный макрос
(define iter 0)
(while (< iter 10) (set! iter (+ iter 1)) (displayln iter )) ; => 1 2 3 …

Определим макрос для цикла for:
(define-syntax for
  (syntax-rules ()
    ((for (iterator from to) body ...)
      (let loop((iterator from))
        (when (< iterator to)
          body ...
          (loop (+ 1 iterator))
        )
      )
    )
  )
)

Проверим:
(for (i 0 15) (displayln i)) ; => 1 2 3 ...


Исключения


В жизни не очень редко появляется необходимость использовать нестабильный код, который во время исполненя может вызвать исключение. В Lisp и в частности Scheme развитая система обработки исключений, ниже приводится простой пример, как можно обрабатывать исключения не опасаясь, что программа потерпит крах.
(guard
  (cond ; переменная c информацией об исключении
    (display (condition-message cond)) ; тело обработчика
  )
  (/ 1 0) ; код вызывающий исключение
)

Мы изучили некоторые из основ языка Scheme описанные стандартом. Конечно, в данной статье приведены далеко не все возможности языка иначе статья получилась бы слишком объемной, по сути это был бы перевод стандарта. Однако, то что мы узнали, вполне достаточно для разработки практически полезных приложений на Scheme.

Комментарии (10)


  1. merhalak
    18.09.2015 11:02

    Racket ругается на конструкцию:

    #\x03BB ; => #\?
    

    03BB: undefined; cannot reference undefined identifier context...: C:\Program Files\Racket\collects\racket\private\misc.rkt:87:7
    Почему, вроде реализация R7RS заявлена?


    1. merhalak
      18.09.2015 11:26

      R6RS, ошибся


    1. Beetle_ru
      18.09.2015 14:01

      Под рукой нет Racket, думаю, причина в том, что в примере юникод, т.е. 2 байта, а Racket поддерживает Ascii, т.е. должно быть #\xE5 для лямбды.


      1. burjui
        18.09.2015 14:27

        $ racket
        
        Welcome to Racket v6.2.1.
        > (? (x) x + 1)
        #<procedure>
        


    1. burjui
      18.09.2015 14:24

      Потому что #\u03BB


      1. merhalak
        18.09.2015 15:52

        Спасибо! Правда в CMD странно выводится.


  1. DjSapsan
    18.09.2015 12:15

    Тут наверное ошибка:

    (and 0 #f); => #f
    (or #f 0); => 0

    Перепутаны or и and
    Ну и на счет самого языка — мне он чем-то напоминает FORTH.


    1. Beetle_ru
      18.09.2015 13:49
      +1

      Ошибки нет, наверно 0 сбивает с толку привыкших к си, в Scheme истина все кроме #f. Результат and и or это либо ложь, либо значение истинного аргумента. Например, (and #f 11 22 33); => #f, при этом как только получен #f остальные аргументы не вычисляются. Выражение (or #f 11 22 33) ;=> 11, т.к. после того, как получена истина следующие аргументы не вычисляются.

      Нечто общее с FORTH есть, сам когда-то увлекался последним. Форт может в более широких пределах расширять синтаксис. Однако, ИМХО лисп более гибок и более высокоуровневый, не нужно постоянно заботится о стеке, проще реализуется переменное число аргументов и т.д…


  1. Daniyar94
    18.09.2015 17:21

    У нас тут ошибка в cond statement, default case должен быть #t, а не else. Хотя может разные интерпретаторы по разному работают?

    Так же забыли упомянуть, что функции вида (define (add a b) (+ a b) это named functions (именные?), а функции вида (lambda (x y) (+ x y)) это anonymous functions.

    Я бы еще сюда добавил функции для создания, разрушение, присоединения листов: cons, car/cdr, append.

    И очень полезную функцию eval


    1. Beetle_ru
      18.09.2015 17:44

      Спасибо за наблюдение. В cond может быть как else так и #t оба варианты корректны, в стандарте R6Rs, в примерах else. Хотя #t наверно более читаемо.

      Так же забыли упомянуть, что функции вида (define (add a b) (+ a b) это named functions (именные?), а функции вида (lambda (x y) (+ x y)) это anonymous functions.

      Я бы еще сюда добавил функции для создания, разрушение, присоединения листов: cons, car/cdr, append.

      И очень полезную функцию eval


      Согласен, как-то упустил из виду, обязательно позже дополню.