Schemat

Schemat
logo
Podstawowe dane
Paradygmaty : Wieloparadygmat : funkcjonalny , proceduralny , meta
Rok wydania: 1975
Projektant: Guy Lewis Steele junior , Gerald Jay Sussman
Deweloper: Guy L. Steele i Gerald Jay Sussman
Aktualna  wersja : R 7 RS (norma ratyfikowana)   (2013)
Pisanie : silny , dynamiczny
Ważne realizacje : wiele z. B. Schemat MIT/GNU
Wpływem: Lisp , ALGOL , MDL
Dotknięty: Wspólne Lisp , JavaScript , R , Rubin , Dylan , Lua , Rakieta , Pstryknięcie ! / BYOB
www.scheme-reports.org

Język programowania Scheme jest odmianą Lisp . Jest funkcjonalny , ale obsługuje również inne paradygmaty programowania (np. programowanie imperatywne ).

Schemat charakteryzuje się tym, że składnia określa tylko kilka koncepcji programistycznych . Istnieje zatem stosunkowo duża liczba sposobów, w jakie program można opisać w Schemacie.

Na przykład w standardzie Scheme nie ma narzędzi do programowania obiektowego, ale bardzo łatwo jest je zaprogramować w tym języku dzięki makrom i wyrażeniom λ : Scheme to programowalny język programowania, który można później rozszerzyć.

Schemat został opracowany przez Geralda Jaya Sussmana i Guya Lewisa Steele Jr. w Massachusetts Institute of Technology , gdzie dostępna jest również formalna specyfikacja , tzw. Revised Report . Aktualna specyfikacja to R 7 RS.

Składnia i semantyka

Składnia Schematu jest bardzo regularne i proste. Podstawą jest notacja przedrostkowa w pełnym nawiasie (patrz też notacja polska ). Program składa się z wyrażeń i definicji. Wyrażenia to literały lub wyrażenia złożone reprezentujące wywołanie funkcji:

  (operator operand-1 operand-2 ... operand-n)

Każdy element wyrażenia złożonego jest z kolei wyrażeniem.

Znaczenie (lub semantykę) wyrażeń określa ich ocena . Znaczenie (semantyka) wyrażenia dosłownego jest stałą wartością wyrażenia. Na przykład ciąg ma 2wartość liczby 2, a ciąg #tma wartość logiczną „prawda”. Podczas obliczania wyrażeń złożonych wyrażenie operator(oparte na operatorach matematycznych ) musi dawać w wyniku funkcję. Na prawo od operatora znajduje się dowolna liczba operandów , które z kolei są formami prostymi lub złożonymi.

Dlatego nawiasy mają szczególne znaczenie i, w przeciwieństwie do większości innych języków programowania, nie można ich ustawiać dowolnie. Forma złożona

(foo 42)

na przykład jest wyrażeniem, które na poziomie semantycznym oznacza wywołanie foofunkcji powiązanej ze zmienną z argumentem 42. Ocena wyrażenia

(foo (42))

jednak prowadzi to do błędu w czasie wykonywania: (42)jest to forma złożona, a semantyka zapewnia użycie operatora. Jednak ponieważ 42jest to liczba, a nie funkcja, pojawia się błąd.

Jedną z zalet notacji przedrostkowej w pełnym nawiasie jest to, że notacja ta ma tylko jeden precedens dla wszystkich operatorów . Powszechna krytyka składni Scheme dotyczy dużej liczby nawiasów, które utrudniają tworzenie i edycję kodu źródłowego. Programiści schematów przeciwdziałają tej trudności, używając edytorów, które w specjalny sposób wspierają przetwarzanie tekstów źródłowych schematów (np. Emacs ). Pomoce te obejmują automatyczne wcinanie kodu i zaznaczanie par nawiasów, które należą do siebie podczas edycji.

Niektóre implementacje Scheme, takie jak Racket, pozwalają na użycie nawiasów kwadratowych oprócz standardu językowego. Ma to na celu zwiększenie przejrzystości. Przykład:

  (let ([x 42]
        [y 23])
     (+ x y))

Forma specjalna lambda

Słowo kluczowe lambda wprowadza specjalną formę dla tak zwanych wyrażeń lambda. Wyrażenie lambda oblicza procedurę, która jest wartością pierwszej klasy w schemacie. Procedury mogą więc być używane w programie jak wartości innych typów, np. powiązane z nazwami, przekazywane jako argumenty w wywołaniu procedury lub zwracane przez procedurę.

Definicja specjalnej formy lambda:

(wyrażenie lambda (argument))

(lambda (x) (* x x)) ; Bildet das Quadrat von x

Wywołanie procedury wygenerowanej przez powyższe wyrażenie lambda:

((lambda(x) (* x x)) 5) ; Bildet das Quadrat von 5
; (Rückgabe = 25)

Nazwa tej specjalnej formy wywodzi się z rachunku lambda .

Definicje globalne

Definicja ze słowem kluczowym definewiąże wartość z nazwą. Nazwa jest powiązana globalnie, więc może być używana w dowolnym miejscu programu po definicji. Ponieważ procedury w Scheme są również wartościami, definemożna również zdefiniować procedury globalne. W poniższej sekcji kodu nazwa jest a-numberpowiązana z liczbą 42, a następnie nazwa jest powiązana squarez funkcją podnoszącą liczbę do kwadratu:

  (define a-number 42)

  (define square
     (lambda (x)
        (* x x)))

Uproszczoną notację można również wykorzystać do zdefiniowania procedur globalnych, w których lambdawyrażenie jest pomijane. Powyższą definicję squaremożna zapisać w formie skróconej w następujący sposób:

  (define (square x)
    (* x x))

Poniższe wyrażenie przedstawia przykład, w jaki sposób funkcja może zwrócić inną funkcję:

(define (sum-with x)
    (lambda (y) (+ y x)))

Parametr x funkcji sum-z określa, jak zachowuje się zwracana funkcja: Zwracana funkcja dodaje argument y dokładnie przez współczynnik x określony w sum-z.

((sum-with 5) 1)
; (Rückgabe = 6)

Lokalne więzi

Deklaracja zmiennych i funkcji w schemacie jest nieco inna niż w tradycyjnych językach programowania. Po pierwsze zmienne i funkcje (procedury) nie muszą być powiązane z identyfikatorami , z drugiej strony deklaracja odbywa się za pomocą procedur, nie ma oddzielnych słów kluczowych.

Dla deklaracji dostępne są formularze define i let ; w razie potrzeby zamiast let można użyć wariacji let * i letrec .

pozwolić

letwiąże kilka wartości z określonym identyfikatorem. Wiązania te są letwidoczne tylko od wewnątrz kadłuba . letma następującą składnię:

 (let ((name-1 ''ausdruck-1'')
       (name-2 ''ausdruck-2'')
       ...
       (name-n ''ausdruck-n''))
   ...
   ; Rumpf des let-Ausdrucks
   ; name-1, ..., name-n sind nur innerhalb des Rumpfes von '''let''' gebunden
   ...
 )

Wyrażenia ausdruck-1do ausdruck-nsą oceniane w nieokreślonej kolejności, zanim wynikowe wartości zostaną powiązane z odpowiednimi nazwami. Następnie letoceniana jest treść wyrażenia. Wiązania name-1do zastosowania tylko w kadłubie name-n. W szczególności nie jest letmożliwe użycie innej nazwy w wyrażeniu, które jest połączone w tym samym wyrażeniu (por . ). ausdruck-iletlet*

Wartość ostatniego wyrażenia w treści daje wartość całego letwyrażenia. Ponieważ kolejność oceny wyrażeń nie jest stała i teoretycznie nawet wszystkie wyrażenia mogą być oceniane w tym samym czasie, mówi się o paraleli . ausdruck-ilet

Przykłady:

 (let ((a 3)
       (b (+ 10 12))
       (c (lambda (n) (* n 2))))
      (c (+ a b)))
 => 50

 (let ((a 1))
      (let ((a 0)
            (b a))
           b))
 => 1

letto uproszczona składnia tłumaczona na wywołanie funkcji. Przykład:

  (let ((a (+ 1 1)) (b (+ 2 2)))
    (+ a b))

rozszerza się do:

  ((lambda (a b) (+ a b)) (+ 1 1) (+ 2 2))

pozwolić *

let*ma taką samą składnię leti podobną semantykę. W przeciwieństwie do let, let*kolejność, w której wyrażenia są oceniane w parach nazwa-wyrażenie, jest stała: ocena jest przeprowadzana od lewej do prawej. Mówi się zatem o sekwencyjnymlet* . W przeciwieństwie do let, w wyrażeniach (po prawej stronie par nazwa-wyrażenie) można uzyskać dostęp do nazw par wiążących dalej w lewo.

Przykład:

 (let ((a 1))
      (let* ((a 0)
             (b a))
            b))
 => 0

How letjest również let*uproszczoną składnią i przekłada się na zagnieżdżone wywołania funkcji:

  (let* ((a (+ 1 1))
         (b (+ a 1)))
    (* a b))

rozszerza się do:

  ((lambda (a)
     ((lambda (b) (* a b)) (+ a 1)))
   (+ 1 1))

letrec i nazwany let

letrecWyrażenia mają taką samą składnię jak letwyrażenia. Istnieją jednak pewne różnice w widoczności wiązanych nazwisk. Nazwy (czyli lewe strony par wiążących) mogą być używane w dowolnym wyrażeniu par wiążących. let*Dobrze znane ograniczenia, że w nazwach wyrażenie może odnosić się tylko do poprzedniej (to znaczy bardziej w lewo), nazwy związku z tym nie może być stosowana. W szczególności może służyć letrecdo definiowania lokalnych funkcji rekurencyjnych. Przykład:

  (letrec ((sum (lambda (lst s)
                 (if (null? lst)
                   s
                   (sum (cdr lst) (+ s (car lst)))))))
    (sum (list 1 2 3) 0))
  => 6

Wzajemnie rekurencyjne funkcje można również zdefiniować:

  (letrec ((even? (lambda (n)
                  (if (zero? n)
                    #t
                    (odd? (- n 1)))))
           (odd? (lambda (n)
                  (if (zero? n)
                    #f
                    (even? (- n 1))))))
    (even? 42))
  => #t

Wariantem letrecjest tzw. namedlet , który jest używany zwłaszcza do programowania pętli. Składnia to

  (let ''name'' (''bindungen'') ''rumpf'')

, gdzie znajdują bindungensię letpary znane z nazwy i wyrażenia. Ciało namedlet jest używane jako treść procedury o nazwie, namektóra ma dokładnie tyle argumentów, ile jest określonych par wiążących. Nazwanylet jest makro, które namerozszerza się do wywołania tej procedury . Jako argumenty używane są prawe strony par wiążących. Przykład dla letrecmoże być napisany z taką nazwąlet :

  (let sum ((lst (list 1 2 3))
            (s 0))
    (if (null? lst)
        s
        (sum (cdr lst) (+ s (car lst)))))

definiować

definejest używany głównie do deklarowania funkcji i stałych na poziomie globalnym, ale może być również używany w ciele procedur. Widoczność tak połączonych zmiennych jest ograniczona do ciała, w którym znajduje się definicja. define, które nie są na poziomie globalnym , otrzymują wewnętrzne definicje i są czasami letrecużywane zamiast jednej dla lepszej czytelności .

Składnia to:

 (define bezeichner ausdruck)

Termin może również dotyczyć identyfikatora rekurencyjnego .

W tym przykładzie fooi barsą zdefiniowane wewnętrznie. Obie zmienne są widoczne tylko w treści letwyrażenia.

  (let ((x 5))

    (define (foo y)
      (bar x y))

    (define (bar a b)
      (+ (* a b) a))

    (foo (+ x 3)))

Powyższy kod odpowiada tej wersji z letrec:

  (let ((x 5))
    (letrec ((foo (lambda (y) (bar x y)))
             (bar (lambda (a b) (+ (* a b) a))))
      (foo (+ x 3))))

Typy danych

Procedury

Procedury są jednym z najważniejszych elementów językowych programu Scheme. Można je opisać wyrażeniem lambda ( lambda ). Ponieważ są one traktowane jak każdy inny typ danych w Scheme, możliwe jest powiązanie ich z identyfikatorem za pomocą let lub define .

Przykład: procedura z dwoma argumentami:

 (define test
   (lambda (arg1 arg2)
         ... ))

Istnieje uproszczona notacja, której można użyć do połączenia wyrażeń define i lambda :

 (define (test arg1 arg2)
  ...)

Ta procedura nazywa się następująco:

 (test wert1 wert2)

Wywołania procedur muszą być na ogół ujęte w dwóch nawiasach. Schemat podkreśla rolę wyrażeń, które mają wartość w porównaniu z poleceniami, które coś robią. Dlatego – w przeciwieństwie do wielu innych języków – nie jest konieczne zaznaczanie wyrażenia w treści opisu procedury jako wartości zwracanej. Wręcz przeciwnie: specjalne konstrukcje, takie jak begin są niezbędne do wykonania instrukcji bez zwracania ich wartości.

Oczywiście istnieje szereg predefiniowanych procedur takich jak cons , car , cdr , + , - , * , < . Te predefiniowane procedury można ponownie zdefiniować, jak pokazano w poniższym przykładzie:

 (define (+ x y)
     (- x y))

+ nie dodałby teraz, ale odejmij.

Ponieważ operatory matematyczne są również implementowane przez procedury, w obliczeniach istnieje notacja przedrostkowa . Schemat nie ma hierarchii operatorów, wszystkie formuły są unikalne.

Pary i listy

Listy są używane stosunkowo często w programach Scheme. Najważniejszym elementem składowym list w programie Scheme są pary . Para to struktura danych, która zawiera dowolne dwie wartości schematu. Wraz z consutworzeniem nowej pary. Przykład:

  (cons 'sinn 42)

To wywołanie constworzy nową parę, której pierwsze pole zawiera symbol, 'sinn a drugie pole numer 42. Dostęp do pierwszego pola pary można uzyskać za pomocą funkcji wbudowanej car(wymawiane: „carr”), drugie pole z cdr(czyt.: "Kudder"). Nazwa car( " C ontent o o DRES r egister"), a cdr( " c ontent z d ecrement r egister") są zakorzenione w historii, ale otrzymuje się w ten sposób. Odnoszą się do operacji, które zostały użyte w pierwszej implementacji Lisp na IBM 704 . Wbudowane funkcje set-car!i set-cdr!ustawiają wartości odpowiednich pól pary na nową wartość. Predykat typu pair?zwraca #t(dla true) wtedy i tylko wtedy, gdy został zastosowany do pary, tj. conswartości, która została wygenerowana za pomocą .

Definicja list jest indukcyjna : lista pusta, napisana '(), jest listą. Również para, która jest cdrlistą, jest listą. Definicja oznacza, że ​​lista składa się z par: W carpolu pary znajduje się dowolna wartość, w cdrpolu para zawierająca kolejny element listy. Koniec listy zostaje osiągnięty, gdy w cdrpolu znajduje się pusta lista, co można null?sprawdzić za pomocą wbudowanej funkcji . Przykłady list:

  '()
  (cons 1 '())
  (cons 1 (cons 2 '()))

Istnieje również wygodna funkcja generowania list list, która pobiera dowolną liczbę argumentów i zwraca je w postaci listy. Poniższe dwie listy są równoważne:

(list 1 2 3)
(cons 1 (cons 2 (cons 3 '())))

Funkcje przetwarzające listy są w większości funkcjami rekurencyjnymi. Funkcji tej można użyć np. do określenia długości listy:

  (define (length lst)
    (if (null? lst)
       0
       (+ 1 (length (cdr lst)))))

Programiści schematów prawie nigdy nie korzystają z możliwości zmiany pól pary za pomocą set-car!lub set-cdr!. Przetwarzanie list jest prawie zawsze czysto funkcjonalne , tj. H. nowe listy są generowane z list. Przykładem jest ta funkcja map, która fstosuje funkcję do wszystkich elementów listy:

  (define (map f lst)
    (if (null? lst)
       '()
       (cons (f (car lst)) (map f (cdr lst)))))

Inne typy danych

Inne typy danych obejmują:

  • liczba całkowita (liczby całkowite, dowolna liczba cyfr)
  • racjonalne (ułamki, dowolna precyzja)
  • liczba rzeczywista ( liczby dziesiętne )
  • Liczby zespolone
  • Symbolika
  • znak
  • Smyczki
  • Pary
  • Wektory
  • Port (reprezentacja urządzeń wejścia/wyjścia, plików itp.)
  • Boole'a

prawda i fałsz są reprezentowane przez #ti #f, przy czym Scheme interpretuje tylko #f(w przestarzałych implementacjach tylko synonim pustej listy '()) jako logicznie fałsz; wszystkie inne wartości są uważane za prawdziwe.

Rozróżnienie przypadku

Gdyby

ifocenia wyrażenie testowe i, w zależności od jego wartości logicznej, ocenia drugi argument ( consequent ) lub trzeci argument ( alternative ). Wartość całego wyrażenia If wynika z oceny następnika lub alternatywy. #fAlternatywa jest oceniana tylko wtedy, gdy wyrażenie testowe ma wartość , w przeciwnym razie kolejne. TJ. każda wartość z wyjątkiem #fjest uważana za logicznie prawdziwą.

Odpowiednie wyrażenie wygląda tak, na przykład:

 (if (> x 0)
    'positive
    'not-positive)

To wyrażenie zwraca albo do symbolu 'positivealbo do symbolu 'not-positive. Ponieważ If jest wyrażeniem, If można używać wewnątrz wyrażeń:

  (* 2 (if (> x max) max x))

Warunek

Dzięki condtemu możliwe jest rozróżnienie kilku przypadków. Przypadku składa się z testu i związany . Przypadki są przeglądane od lewej do prawej. Jeśli test nie #fzwróci przypadku , obliczana jest odpowiednia konsekwencja: ta wartość daje następnie wartość całego condwyrażenia. Jeśli żaden z podanych przypadków nie ma zastosowania, oceniany jest inny case , jeśli istnieje . Jeśli nie ma innego przypadku, wartość condwyrażenia nie jest zdefiniowana. Przykład:

 (cond ((= wert 65) 'a)
       ((= wert 66) 'b)
       (else 'unbekannt))

wstążki

Scheme nie posiada żadnych konstrukcji języka programowania dla pętli (jednak automatycznie włączane „Biblioteki schematów standardowych” oferują możliwość tworzenia pętli na przykład z konstrukcją do). Pętle są zwykle implementowane przy użyciu rekurencyjnych wywołań funkcji . W najprostszym przypadku nieskończona pętla wygląda tak:

 (define (loop)
  (loop))

Przykładem często pokazywanym, aby to zademonstrować, jest obliczenie silni :

 (define (fak n)
    (if (= n 0) 1
        (* n (fak (- n 1)))))

W celu praktycznej realizacji tej teoretycznie atrakcyjnej koncepcji, Scheme optymalizuje rekurencję trzpienia końcowego (lub bardziej ogólnie: wszystkie wywołania funkcji trzpienia końcowego). W przypadku nieterminalnej rekurencji funkcja nadal działa po samo wywołaniu . W przykładzie mnożenie:

 (fak 4)
 (* 4 (fak 3))
 (* 4 (* 3 (fak 2)))
 (* 4 (* 3 (* 2 (fak 1))))
 (* 4 (* 3 (* 2 (* 1 (fak 0)))))
 (* 4 (* 3 (* 2 (* 1 1))))
 (* 4 (* 3 (* 2 1)))
 (* 4 (* 3 2))
 (* 4 6)
 24

W trakcie procesu potrzeba coraz więcej miejsca (pamięci) do zapisania wyników pośrednich. Ogonowo-rekurencyjny wariant powyższego przykładu byłby następujący:

 (define (fak-iter n a)
  (if (= n 0) a
      (fak-iter (- n 1) (* n a))))

 (define (fak n)
  (fak-iter n 1))

Proces wyglądałby wtedy tak:

 (fak 4)
 (fak-iter 4 1)
 (fak-iter 3 4)
 (fak-iter 2 12)
 (fak-iter 1 24)
 (fak-iter 0 24)
 24

Scheme rozpoznaje, że wynik wywołania procedury jest zwracany tylko i dlatego może używać tej samej pamięci dla wszystkich wywołań procedury. Dodatkowa zmienna a w procedurze fak-iter akumuluje wyniki pośrednie.

Uwagi

Komentarze są poprzedzone średnikiem (;) i rozciągają się do końca linii.

Porównanie programu Scheme i LISP

Istnieją trzy główne cechy, które odróżniają Scheme od Lisp :

  • W Scheme jest funkcja call-with-current-continuation . Pozwala na użycie bieżącej kontynuacji (tj. rodzaju stanu wykonania bieżącego programu ) jako zmiennej . Umożliwia to powrót do punktu tej kontynuacji w dalszej części przebiegu programu.
  • Standard Scheme określa właściwą rekursję ogonową : wywołania procedur w końcowej pozycji rekurencyjnej nie mogą zajmować miejsca w pamięci na stosie wywołań programu. Oznacza to, że możliwa jest nieograniczona liczba końcowych rekurencyjnych wywołań procedury.
  • W przeciwieństwie do LISP-a, makra w Scheme są „higieniczne”: identyfikatory wprowadzone w makrze nie ukrywają żadnych innych identyfikatorów w środowisku leksykalnym poza makro, tj. programem wywołującym. I odwrotnie, identyfikatory używane w makrze są zawsze rozwiązywane w środowisku leksykalnym makra, a nie na zewnątrz. Oznacza to, że identyfikator makra jest widoczny tylko dla samego makra i makro nie może uzyskać dostępu do identyfikatora programu wyższego poziomu, a program nie może uzyskać dostępu do wewnętrznego identyfikatora makra.

Wdrożenia i narzędzia programistyczne

Dostępna jest duża liczba implementacji języka programowania. Poniżej wymieniono tylko kilka popularnych implementacji:

  • Bigloo tłumaczy kod Scheme na kilka innych języków: C , Java i .NET .
  • Chicken to implementacja, która tłumaczy Scheme na C i dlatego działa na praktycznie wszystkich platformach. Zapewnia zarówno interpreter, jak i kompilator, a dzięki połączeniu z C posiada obszerną bibliotekę rozszerzeń. Implementacja jest w dużej mierze zgodna z R5RS.
  • Oprócz interpretera Scheme , Gambit C posiada również jeden z najbardziej wydajnych kompilatorów Scheme-to-C .
  • Gauche to implementacja zgodna z R5RS. Został zaprojektowany jako interpreter skryptów dla programistów i administratorów. Niektóre z celów rozwoju to krótki czas uruchamiania, wbudowany interfejs systemu, natywna obsługa wielu języków. Ponadto można zintegrować liczne rozszerzenia, m.in. B. dla OpenGL i GTK+ .
  • GNU Guile to interpreter, który można zintegrować jako bibliotekę. Podstawowym celem jest możliwość łatwego dodawania funkcji skryptów do programów.
  • LispMe jest implementacją dla PDA kompatybilnych z PalmOS.
  • Racket (wcześniej PLT Scheme) to szeroko rozpowszechniona implementacja, która oprócz dużej liczby bibliotek zawiera własne środowisko programistyczne o nazwie DrRacket (wcześniej DrScheme). DrRacket jest specjalnie przystosowany do użycia w treningu programowania dla początkujących i jest łatwy w użyciu. Racket zawiera kilka kompilatorów, które konwertują kod Racket / Scheme na kod bajtowy lub C.
  • Schemat 48 to implementacja Schematu napisana w całości w schemacie. Statycznie wpisane dialekt Program nazywa PreScheme jest używany do ładowania początkowego . Schemat 48 tłumaczy kod schematu na kod bajtowy, który można zapisać w obrazie pamięci. Schemat 48 jest szczególnie godny uwagi ze względu na jego zdolność do debugowania kodu schematu.
  • SIOD. Mały, szybki interpreter schematów, który był używany w GIMP- ach ScriptFu do wersji 2.4.

literatura

linki internetowe

Przedstawienia

Standardy językowe

Indywidualne dowody

  1. strona standardu R7RS
  2. Lista znanych wdrożeń
  3. Strona internetowa Bigloo
  4. Strona internetowa Gambit C
  5. Strona internetowa Gauche
  6. Strona internetowa LispMe
  7. Strona internetowa o rakietach
  8. Strona internetowa Schematu 48
  9. strona SIOD ( pamiątka z oryginałem z dnia 6 kwietnia 2007 w Internet Archive ) Info: archiwum Link został wstawiony automatycznie i nie została jeszcze sprawdzona. Sprawdź link do oryginału i archiwum zgodnie z instrukcjami, a następnie usuń to powiadomienie. @1@2Szablon: Webachiv / IABot / www.cs.indiana.edu