programming languages – Hamel’s Blog - Hamel Husain
Хамел Хусейн делится главными выводами после прохождения трёхчастного курса Coursera «Programming Languages» Дэна Гроссмана; код его (приватного) репозитория лежит на GitHub. В части A изучается Standard ML (SML) — статически типизированный язык с мощным выводом типов, рекурсией вместо циклов, сопоставлением с образцом через case, замыканиями, хвостовой рекурсией, каррированием и композицией функций. Часть B посвящена Racket (родственнику Lisp и Scheme) с динамической типизацией, thunk'ами, ленивыми вычислениями, макросами и написанием собственного маленького интерпретатора. В части C рассматривается Ruby — чисто объектно-ориентированный язык с динамической типизацией, блоками, yield, лямбдами и наследованием. Отдельно автор описывает настройку Vim как IDE для SML с плагином jez/vim-better-sml и конфигурацией .vimrc.
Главные выводы после прохождения трёхчастного курса Coursera «Programming Languages» с Дэном Гроссманом.
Ваш (приватный) GitHub-репозиторий для этого курса находится здесь.
SML (Standard ML), часть A
Вы настроили vim как IDE для этого. См. заметки в разделе VIM ниже. ML — статически типизированный язык с волшебным выводом типов, который работает действительно хорошо. Он автоматически определяет типы и очень интуитивен и полезен. Научились использовать рекурсию повсюду вместо циклов, в частности с hd, tl и cons. Очень важно локальное связывание переменных через let (которое также позволяет связывать локальные/приватные функции). cons позволяет добавлять элемент в начало списка. Есть тип option, который равен NONE или SOME v. Этот язык не поощряет мутацию, и это его достоинство. В противном случае можно использовать ссылку, которая подобна указателю, для мутации переменной. Сопоставление с образцом через выражение case: это одна из самых классных вещей, которые я узнал, и нечто похожее появляется в Python 3.10. Можно использовать вложенные образцы. Можно сопоставлять с образцом аргументы функций, что даёт очень приятный синтаксис для реализации функциональности вроде множественной диспетчеризации.. (насчёт python не уверен). Можно сопоставлять с образцом как типы, так и структуры данных. Можно использовать там и константы. case name NameType name => ... | (first, "MyLastName") => ... | (first, last) => ... | name => ... | _ => ... Хвостовая рекурсия с аккумуляторами. Пример — факториал. Ключевое слово fn используется для определения анонимных функций. ML использует лексическую область видимости, что означает, что функция вычисляется в окружении, где она была определена. динамическая область видимости, которая обычно нежелательна, — это альтернатива, при которой функция вычисляется в окружении, где она вызывается. Замыкание — в стеке вызовов есть «пара», представляющая собой (функцию, окружение на момент определения функции). Эта пара называется замыканием. Стек вызовов содержит снимок того, как выглядело окружение в момент определения функции. fold похож на reduce. ML поддерживает композицию функций так, с ключевым словом o: f1 o f2 o f3. Лучше сделать val-связывание, чтобы избежать ненужной обёртки: val newfunc = f1 o f2. С o функции применяются справа налево, так что f1 o f2 x — это то же самое, что f1(f2(x)). Есть альтернатива, применяющая слева направо, называемая оператором конвейера (pipeline operator). Каррирование и частичное применение. Универсальный способ сделать функцию каррируемой: ml fun myfunc x let fun f2 (z) = z fun f1 (y) = f2(y) begin f1 end. ML имеет первоклассную поддержку каррирования, так что прибегать к описанному выше трюку не нужно. ML поддерживает взаимную рекурсию так же, как let-rec в racket.
Racket (часть B)
Racket родственен Lisp и Scheme. Всё является функцией. Скобки для всего. Положение скобок меняет смысл кода.
В Racket динамическая типизация, в отличие от SML. Thunk'и: оборачивают функцию в функцию без аргументов, чтобы отложить вычисление. Применения: Потоки (Streams): функция возвращает кортеж (значение, функция), и когда вы вызываете функцию, она возвращает (значение, функция), так что вы получаете по одному значению за раз. Это не специфично для Racket. Ленивые вычисления: можно использовать thunk'и, чтобы отложить выполнение, как обещание, на более поздний момент. Вот пример ленивого вычисления, которое фактически ничего не вычисляет, пока его к этому не принудят:
(define (my-delay f) (mcons #f f)) (define (my-force th) (if (mcar th) (mcdr th) (begin (set-mcar! th #t) (set-mcdr! th ((mcdr th))) (mcdr th))))
Racket позволяет использовать макросы, которые вычисляются до запуска кода и «разворачиваются» в валидный синтаксис racket.
Вы реализовали собственный небольшой язык программирования. Здесь использовались рекурсивные вызовы для вычисления выражений, где базовым случаем являются значения (целые числа, строки и т. д.). - Интерпретатор: написать на другом языке A программу, которая принимает программы на B и сразу выдаёт ответы. Более точным термином было бы «вычислитель» (evaluator). - Компилятор: написать на другом языке A программу, которая принимает программы на B и выдаёт эквивалентную программу на языке C. Более точным термином здесь было бы «транслятор» (translator).
Замыкания: для лексической области видимости интерпретатор имеет стек кортежей. Кортежи — это (1) вызываемая функция (2) окружение, которое содержит значения всех переменных на момент определения функции. Также нужно отслеживать аргументы функции отдельно, чтобы можно было вычислять их в окружении, в котором функция была запущена.
Ruby (часть C)
Я не потратил слишком много времени, с некоторыми концепциями я был в основном знаком.
Ruby — объектно-ориентированный язык с динамической типизацией. Ruby — это чистый ООП, даже функции и переменные верхнего уровня являются частью встроенного класса Object. В нём есть короткие способы вроде fastcore для геттеров и сеттеров:
attr_reader :y, :z # определяет геттеры attr_accessor :x # определяет геттеры и сеттеры
Переводы строк важны. Без них синтаксис может измениться.
Динамические определения классов. Следующий код приведёт к классу Class с методами foo и bar! Второй не переопределяет первый!
class Class def foo ... end end class Class def bar ... end end
Блоки (Blocks)
В нём также есть очень удобная штука, похожая на лямбды, под названием Blocks:
sum = 0 [4,6,8].each { |x| sum += x puts sum }
Можно использовать Blocks и для создания аккумуляторов, и даже использовать inject для инициализации аккумулятора:
sum = [4,6,8].inject(0) { |acc,elt| acc + elt }
Чтобы использовать блоки внутри метода, придётся поискать это в документации. Это связано с ключевым словом yield. Например, этот код напечатает «hi» 3 раза:
def foo x if x yield else yield yield end end foo (true) { puts "hi" } foo (false) { puts "hi" }
Blocks не являются первоклассными функциями, хотя и выглядят немного как лямбды. Допустим, вы хотите применить map к массиву, но хотите вернуть массив функций вместо значений. Способ сделать это — использовать ключевое слово lambda:
c = a.map {|x| {|y| x >= y} } # неверно, синтаксическая ошибка c = a.map {|x| lambda {|y| x >= y} } # это сработает
Наследование (Subclassing)
super вызывает тот же метод в родительском классе. Не нужно писать super.method_name(), достаточно просто super. Переменные экземпляра предваряются символом @
Дочерние классы определяются так:
class Child < Parent ... end
Типизация
Обсуждались различные способы построения систем типов. Была представлена идиома интерфейсов, знакомая вам по Golang (но не специфичная для Golang).
VIM
Для языка программирования Standard ML я решил заставить себя использовать vim. Я добавил следующие вещи в свой .vimrc, чтобы сделать работу управляемой. Обратите внимание на плагин jez/vim-better-sml
" from https://github.com/jez/vim-as-an-ide set nocompatible inoremap