Функциональные языки программирования: LISP (Scheme/Racket) + Haskell. Использование рекурсии для решения задач

Функциональное программирование — раздел дискретной математики и парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании (в отличие от функций как подпрограмм в процедурном программировании). У функции в математике есть аргументы и результат, она не меняет состояние остальной программы (отсутствуют побочные эффекты).

Противопоставляется парадигме императивного программирования, которая описывает процесс вычислений как последовательное изменение состояний (как в теории автоматов).

Функциональное программирование предлагает обходиться вычислением результатов функций от исходных данных и результатов других функций, и не предполагает явного хранения состояния программы. Соответственно, не предполагает оно и изменяемость этого состояния (в отличие от императивного, где одной из базовых концепций является переменная, хранящая своё значение и позволяющая менять его по мере выполнения алгоритма).

Сильная (строгая) и слабая типизация

Слабая типизация - операции приведения типов. Строгая - надо явно приводить тип.

Статическая типизация - все типы известны на этапе компиляции.

Динамическая типизация - переменная может менять тип в процессе выполнения программы, на этапе компиляции типы неизвестны.

 

Сформулированная Джоном Мак-Карти (1958) концепция символьной обработки информации компьютером восходит к идеям Черча и других математиков, известным как лямбда-исчисление с конца 20-х годов XX века.

Выбирая лямбда-исчисление как теоретическую модель, Мак-Карти предложил рассматривать функции как общее базовое понятие, к которому достаточно естественно могут быть сведены все другие понятия, возникающие при программировании.

Существуют различия в понимании  функции в математике и функции в программировании, вследствие чего нельзя отнести Си-подобные языки к функциональным, использующим менее строгое понятие. Функция в математике не может изменить вызывающее её окружение и запомнить результаты своей работы, а только предоставляет результат вычисления функции.

Программирование с использованием математического понятия функции вызывает некоторые трудности, поэтому функциональные языки, в той или иной степени предоставляют и императивные возможности, что ухудшает дизайн программы (например возможность безболезненных дальнейших изменений).

Дополнительное отличие от императивных языков программирования заключается в декларативности описаний функций. Тексты программ на функциональных языках программирования описывают «как решить задачу», но не предписывают последовательность действий для решения.

Первым функциональным языком стал Лисп. Он был предложен Джоном Мак-Карти в качестве средства исследования границ применимости компьютеров, в частности, методом решения задач искусственного интеллекта.

Вариант данного языка широко используется в системе автоматизированного проектирования AutoCAD и называется AutoLISP

Основные преимущества функциональных языков:

Краткость и простота

Программы на функциональных языках обычно намного короче и проще, чем те же самые программы на императивных языках.
Пример (быстрая сортировка Хоара на абстрактном функциональном языке):

quickSort ([]) = []
quickSort ([h : t]) = quickSort ([n | n ∈ t, n <= h) + [h] + quickSort ([n | n ∈ t, n > h])

Строгая типизация

В функциональных языках большая часть ошибок может быть исправлена на стадии компиляции, поэтому стадия отладки и общее время разработки программ сокращаются. Вдобавок к этому строгая типизация позволяет компилятору генерировать более эффективный код и тем самым ускорять выполнение программ.

Модульность

Механизм модульности позволяет разделять программы на несколько сравнительно независимых частей (модулей) с чётко определёнными связями между ними. Тем самым облегчается процесс проектирования и последующей поддержки больших программных систем. Поддержка модульности не является свойством именно функциональных языков программирования, однако поддерживается большинством таких языков.

Функции — объекты вычисления

В функциональных языках (равно как и вообще в языках программирования и математике) функции могут быть переданы другим функциям в качестве аргумента или возвращены в качестве результата. Функции, принимающие функциональные аргументы, называются функциями высших порядков или функционалами.

Чистота (нет побочных эффектов)

В чистом функциональном программировании оператор присваивания отсутствует, объекты нельзя изменять и уничтожать, можно только создавать новые путем декомпозиции и синтеза существующих. О ненужных объектах позаботится встроенный в язык сборщик мусора. Благодаря этому в чистых функциональных языках все функции свободны от побочных эффектов.

Отложенные (ленивые) вычисления

В императивных языках программирования (например, C++) вызов функции приводит к вычислению всех аргументов. Этот метод вызова функции называется вызов-по-значению. Если какой-либо аргумент не использовался в функции, то результат вычислений пропадает, следовательно, вычисления были произведены впустую. Противоположностью вызова-по-значению является вызов-по-необходимости (ленивые вычисления). В этом случае аргумент вычисляется, только если он нужен для вычисления результата.

LISP

Скачивание и установка

Скачать Racket: http://racket-lang.org

Программы и данные неотличимы друг от друга — всё есть списки.

Программа на языке LISP — список. Первый элемент списка — имя функции которую надо вызвать, остальные — аргументы.

       (действие аргумент1 аргумент2 ...)
    

Сначала вычисляются аргументы.

Функция list конструирует список:


        (list 1 2 3)
    

Условный оператор (больше похож на тернарный)

      (if условия если_верно если_нет)
    

Объявление функции

      (define (имя_функции арг1 арг2) (код))
    

Объявление константы

      (define pi 3.14)
    

Литералы списков:

    '()  
    '(2 3 4)
    

Конструирование списка из головы и конца:

    (cons head tail)
    

Голова списка

    (car L)
    

Хвост списка

    (cdr L)
    

Разберем и построим снова список L:

    (cons (car L) (cdr L))
    

Арифметические функции: + - * /

null? — пуст ли список?

Вычисление длины списка

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

Получить элемент списка L с номером n

    (define (get L n)
      (if (= n 0)
          (car L)
          (get (cdr L) (- n 1))
      )
    )
    
Факториал
    (define (fact n)
      (if (= n 0)
          1
          (* n (fact (- n 1)))
      )
    )
    
Применить одну операцию ко всем элементам списка
    (define (map F L)
      (if (null? ()
          '()
          (cons (F (car L) (map F (cdr ())))
      )
    )
    

(display ... out) — вывод на экран

(read in) — чтение

(close-output-port out) — закрытие файла (нужно при работе в среде разработки)

Тексты программ на функциональных языках программирования описывают «как решить задачу», но не предписывают последовательность действий для решения. 

Первым спроектированным функциональным языком стал Лисп

Лисп/LISP (LISt Processing language - «язык обработки списков») — семейство ЯП, программы и данные в которых представляются системой вложенных списков.

Лисп является вторым в истории (после Фортрана) используемым по сей день высокоуровневым языком программирования. 

http://ru.wikipedia.org/wiki/Лисп

Haskell

Haskell (Хаскель, Хаскелл) - стандартизованный чистый функциональный ЯП общего назначения (один из самых распространённых с поддержкой отложенных/ленивых вычислений). Типизация строгая, статическая, с автоматическим выводом типов.

Концепция языка отражает идею математика Хаскелла Карри, писавшего, что «доказательство — это программа, а доказываемая формула — это тип программы». Именно в честь Х. Карри язык и получил своё название.

Решение задач на LISP

#lang racket

;; Демонстрация стандартных функций LISP
;; display вывод на экран 
(display "Это LISP :) ")
(display "Тут можно складывать сразу много чисел: ")
;; Первый параметр списка - функция, остальные - аргументы
(+ 1 2 4 -10)
(display " ..и умножать тоже... ")
(* 2 3 4)
(display ".. вычитаем из первого аргумента все остальные..")
(- 10 5 5)
(display ".. делим первый аргумент на все остальные..")
(/ 1 2 3)

(display "Остаток от деления 5 на 3: ")
(remainder 5 3)

(display "Логические операции: ")
(display "Логическое И: ")
(and #f #f) (and #f #t) (and #t #f) (and #t #t)

(display "Логическое ИЛИ: ") (newline)
(or #f #f) (or #f #t) (or #t #f) (or #t #t)

(display "Сравнение (операция равно): ")
(eq? 2 1) 

(display "Не равно: ")
(not (eq? 2 1))

Наибольший общий делитель

#lang racket
; Открываем входной файл как in
(define in (open-input-file "gcd.in"))
; Открываем выходной файл как out, перезаписываем его если он уже есть
(define out (open-output-file "gcd.out" #:exists 'replace))
; Функция gcd, если y равен нулю, то возвращает x,
; иначе вызывает саму себя с параметрами y и остаток от деления x на y
(define (gcd x y) (if (zero? y) x (gcd y (remainder x y))))
; Считываем два числа из входного файла, передаём их на вход функции gcd и выводим в выходной файл ответ
(display (gcd (read in) (read in)) out)
; Сбрасываем буфер вывода
(flush-output out)

Разворот списка

#lang racket
(define (R L)
  (if (null? L) ;; Если список пустой,
      L  ;; то возвращаем его же
      (append (R (cdr L)) ;; переворачиваем конец
              (list (car L))
              )
      )
  )

;; Тесты
(R '())
(R '(1))
(R '(1 2)) ;; '(2 1)
(R '(1 2 3))
(R '(234 34 2 3))

Скобки

#lang racket

;; Открывающая скобка
(define (openBracket x)
  (or (eq? x #\() (eq? x #\[) (eq? x #\{)))

;; Закрывающая скобка
(define (closeBracket x)
  (or (eq? x #\)) (eq? x #\]) (eq? x #\})))


;; Правильная закрывающая скобка
(define (match open close)
  (or (and (eq? open #\() (eq? close #\)))
      (and (eq? open #\[) (eq? close #\]))
      (and (eq? open #\{) (eq? close #\}))
      )
  )



(define (check L s)
  (if (null? L)
      (null? s)
      (if (openBracket (car L)) ;; Открывающая скобка
          (check (cdr L) (cons (car L) s))
          (if (closeBracket (car L))
              (if (null? s)
                  #f ;; Пустой стек
                  (and (match (car s) (car L)) ;; На вершине стека должна быть нужная скобка => продолжаем
                      (check (cdr L) (cdr s)) ;; И дальше всё в порядке
                      )
                  )
              #f ;; Неверный символ
              )
          )
      )
  )

(define (checkStr s)
  (check (string->list s) '()))

;; Тесты
(checkStr "")
(checkStr "()")
(checkStr "(())")
(checkStr "([])")
(checkStr "([{}])")
(checkStr "(]")
(checkStr "(()")

Быстрое возведение в степень

#lang racket

;; a в степени b
(define (pow a b)
  (if (eq? b 0)
      1  ;; a^0 = 1
      (if(eq? (remainder b 2) 0) ;; Если b - чётная
         (* (pow a (/ b 2)) ;; то делим степень на 2
            (pow a (/ b 2))
            )
         (* a (pow a (- b 1)))
         )
      )
  )

(pow 2 0)
(pow 2 1)
(pow 2 2)
(pow 2 3)

Обратная польская нотация

#lang racket
; Открываем входной файл как in
(define in(open-input-file"rpn.in"))
; Открываем выходной файл как out, перезаписываем его если он уже есть
(define out(open-output-file"rpn.out"#:exists 'replace))

; Выполнение операции
(define (do f e s operation)
  (f
   (cdr e)
   (cons (operation (car (cdr s)) (car s)) (cdr (cdr s)))))

; Функция f: e - оставшаяся часть выражения, s - стек
(define (f e s)
  (if(null? e)
     (car s)
     (if(eq? (car e) '+) (do f e s +)
        (if(eq? (car e) '-) (do f e s -)
           (if(eq? (car e) '*) (do f e s *)
              (if(eq? (car e) '/) (do f e s /)
                 (f (cdr e) (cons (car e) s)))
              )))))

; Считываем из входного файла, подаём на вход функции f и выводим результат в выходной файл
(display (f (read in) '()) out)
; Сбрасываем буфер вывода
(flush-output out)
#lang racket
; Открываем входной файл как in
(define in(open-input-file"rpn.in"))
; Открываем выходной файл как out, перезаписываем его если он уже есть
(define out(open-output-file"rpn.out"#:exists 'replace))

; Выполнение операции
(define (do f e s operation)
  (f
   (cdr e)
   (cons (operation (car (cdr s)) (car s)) (cdr (cdr s)))))

; Функция f: e - оставшаяся часть выражения, s - стек
(define (f e s)
  (if(null? e)
     (car s)
     (if(eq? (car e) '+) (do f e s +)
        (if(eq? (car e) '-) (do f e s -)
           (if(eq? (car e) '*) (do f e s *)
              (if(eq? (car e) '/) (do f e s /)
                 (f (cdr e) (cons (car e) s)))
              )))))

; Считываем из входного файла, подаём на вход функции f и выводим результат в выходной файл
(display (f (read in) '()) out)
; Сбрасываем буфер вывода
(flush-output out)

Haskell - строгая типизация