LINUX.ORG.RU

Анализ пользователей Common Lisp и Racket

 , ,


11

7

Common Lisp разрабатывался и используется в предположении, что пользователь программы — программист. Поэтому из языка намеренно исключены сложные для понимания конструкции (пользователь не обязательно квалифицированный программист), поэтому в языке мощнейший отладчик, позволяющий без остановки программы переопределять функции и вообще делать что угодно. Но из-за этого документация по большей части библиотек Common Lisp существует только в виде docstring и комментариев в коде (некоторые вообще считают, что код сам себе документация). Из-за этого обработка ошибок почти всегда оставляется на отладчик (главное сделать рестарт «перезапустить с последней итерации», а там пользователь сам разберётся). Из-за этого в программе проверяется только happy path (пользователь ведь «тоже программист»).

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

И поэтому в Racket нет CLOS (есть как минимум две реализации, но не используются) - провоцирует заплаточное программирование (monkey patching), поэтому отладчик намеренно ограничен (если ты отлаживаешь программу, значит ты не знаешь как она должна работать!), поэтому нет разработки в образе (image based) - она провоцирует разработку через отладку (а значит непонимание программы и проверку только happy path).

Таким образом, Racket и Common Lisp несмотря на внешнее сходство являются очень разными языками. И я рекомендую писать на Racket, если только конечными пользователями программы не являются исключительно программисты на Common Lisp.

Взято с http://racket-lang.blog.ru/#post214726099

Хотелось бы знать, что по этому поводу думают пользователи ЛОРа. А также, мне кажется, что для Java и C++ будет где-то такая же разница.

★★★★★

Ответ на: комментарий от monk

Даже в более простом случае «либо число, либо строка» приходится data и конструкторы делать.

Это если система типов не умеет в объединения. А если умеет то (U Integer String). Но тут нужны еще и типы-отрицания (разности).

anonymous
()
Ответ на: комментарий от monk

Транзакции на обновление (см. rpm и прочие пакетные менеджеры). Для тотального Type Inference единственный вариант.

This. Правда, type inference, тотальный, локальный, или ещё какой-то, здесь совершенно не причём.

tailgunner ★★★★★
()
Ответ на: комментарий от anonymous

Как раз для того, чтобы обходиться без SQL, и придумали ОРМ.

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

turtle_bazon ★★★★★
()
Ответ на: комментарий от x4DA

а я про вывод типов при компиляции

Оно и есть. Вполне себе выводятся. Не так круто, конечно, конечно, как в хаскеле, но вполне. Ну и от реализации, конечно, зависит. Где его нет, где CP, а где TI.

turtle_bazon ★★★★★
()
Ответ на: комментарий от anonymous

Надо понимать что тайпинференс - это не инструмент практика, а выебон теоретика.

Не, на практике этот TI очень удобен.

turtle_bazon ★★★★★
()
Ответ на: комментарий от tailgunner

Правда, type inference, тотальный, локальный, или ещё какой-то, здесь совершенно не причём

Если нет глобального вывода типов, то достаточно версионирования. При обновлении .so мне не приходится перезагружать Linux и не приходится заворачивать доступ к библиотекам в транзакцию. Всё, что уже запущено использует старую версию, то что перезапускается использует новую версию.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Это если система типов не умеет в объединения. А если умеет то (U Integer String).

Мы про Haskell. Там объединения только явные через data T = T1 Type1 | T2 Type2. Поэтому без конструкторов никак.

А с полным выводом типов нормальные объединения не совместимы, так как становится неоднозначным тип аргумента в полиморфных функциях.

monk ★★★★★
() автор топика
Ответ на: комментарий от turtle_bazon

Не, на практике этот TI очень удобен.

Если как в SBCL/Typed Racket, то да. Если как в *ML/Haskell, то иногда больше мешает чем помогает.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Если нет глобального вывода типов, то достаточно версионирования.

(пожимая плечами) если ты определишь границы версионированных компонентов каким-то способом - конечно, достаточно.

При обновлении .so мне не приходится перезагружать Linux

Причём тут перезагрузка Linux? Ты должен перезапустить приложение.

и не приходится заворачивать доступ к библиотекам в транзакцию.

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

tailgunner ★★★★★
()
Ответ на: комментарий от turtle_bazon

Не, на практике этот TI очень удобен.

Он удобен для того чтобы код писать. И по-этому время программиста практически не экономит, потому что написание кода - это проценты от всех трудозатрат. Ведь работа программиста - в основном читать код, а не писать.

anonymous
()
Ответ на: комментарий от monk

Мы про Haskell.

Нет, мы не про хаскель, мы про статическую типизацию. Если у хаскеля там есть какие-то болезни - это проблема хаскеля.

А с полным выводом типов нормальные объединения не совместимы

Ну и хрен с ним, полный вывод типов все равно вреден.

anonymous
()
Ответ на: комментарий от anonymous

Какой толк от такого хотпатчинга?

Позволяет разрабатывать в стиле CL. Загрузил функцию, проверил, загрузил новую версию функции с другой сигнатурой, проверил. Когда все функции перевёл на новую версию (по одной), запустил «перекомпилировать всё изменённое» чтобы проверить консистентность все программы. И всё это не выходя из REPL'а.

monk ★★★★★
() автор топика
Ответ на: комментарий от tailgunner

Причём тут перезагрузка Linux? Ты должен перезапустить приложение.

Так и здесь. Если я обновил функцию, а она где-то используется то новая версия будет использоваться при перезагрузке использующей функции.

транзакция начинается при открытии so-файла

А здесь при компиляции функции. До следующей перекомпиляции.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Нет, мы не про хаскель, мы про статическую типизацию.

Пройди по треду. Началось с Анализ пользователей Common Lisp и Racket (комментарий)

покажи мне сигнатуру рекурсивного списка на хаскеле (с неизвестным на этапе компиляции уровнем вложенности)

А если про статическую типизацию вообще, то C, С++, Haskell и Typed Racked имеют сильно разное представление о том, что такое «тип».

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Причём тут перезагрузка Linux? Ты должен перезапустить приложение.

Так и здесь.

Ну то есть перезагрузка Linux таки не причём.

транзакция начинается при открытии so-файла

А здесь при компиляции функции.

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

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

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

Напоминаю контекст вопроса: возможно ли в языках со статической типизацией обновлять функции в REPL без останова программы.

Никто не мешает работать в стиле

> (define (f (x : Integer)) (+ x 1))
> (define (g (x : Integer)) (+ (f x) 1))
> (g 1)
3
; обновляем f

(update (f (x : Integer) (y : Integer)) (+ x y))
(f 1 2)

3

(g 1)

3 Здесь f уже новая, но g использует старую версию

> (update (g (x : Integer)) (+ (f x 1) 1))
Теперь в g используется новая версия f.

Если сигнатура не меняется тогда просто

> (update g)
для обновления g или
> (update-all-changed)
Для обновления ссылок во всех функциях.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Напоминаю контекст вопроса: возможно ли в языках со статической типизацией обновлять функции в REPL без останова программы.

Вообще-то речь шла о горячей замене кода. REPL был приведен как пример «в статически типизированных языках вполне можно реализовать eval()».

Никто не мешает работать в стиле

Я понимаю, что у тебя есть какие-то идеи по поводу реализации горячей замены кода, но не ссылайся на них, пока они только у тебя в голове.

tailgunner ★★★★★
()
Ответ на: комментарий от x4DA

Список это список одинаковых элементов если там либо либо то тогда уж

data F a = F (Either a [F a])

А тут уж никаких проблем как ты понимаешь нет.

qnikst ★★★★★
()
Ответ на: комментарий от monk

Попробуй понять то что я писал тебе выше и прекрати писать чепуху про вредностб вывода типов, и то что тайпчеккер что-то угадывает - не угадывает он.

qnikst ★★★★★
()
Ответ на: комментарий от monk

я могу в хацкеле заменить функцию не пересобирая код, дальше что?

qnikst ★★★★★
()
Ответ на: комментарий от monk

Позволяет разрабатывать в стиле CL. Загрузил функцию, проверил, загрузил новую версию функции с другой сигнатурой, проверил.

Не, это весьма костыльный вариант.

anonymous
()
Ответ на: комментарий от qnikst

Список это список одинаковых элементов если там либо либо то тогда уж

Нет, не одинаковых, а произвольных.

data F a = F (Either a [F a])

Только надо минус-типы, то есть: data F a = (not F) a => F (Either a [F a])

иначе не получится

anonymous
()
Ответ на: комментарий от qnikst

Попробуй понять то что я писал тебе выше и прекрати писать чепуху про вредностб вывода типов, и то что тайпчеккер что-то угадывает - не угадывает он.

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

anonymous
()
Ответ на: комментарий от qnikst

Не должен. Не пытается. Задолбали.

Если бы не пытался, то он бы не вывел НИКАКОГО типа. Когда человек не знает ответа наверняка и наывает какой-то ответ исходя из косвенных соображений, без гарантий того, что ответ правильный, то мы что говорим? «пытался угадать». Вот и тайпчекер делает то же самое - не знает ответ, не имеет никаокг оспособа его получить, но пытается «угадать» из косвенных соображений. Ну типа раз в коде возвращается некий тип Х, то, наверное, ф-я должна возвращать тип Х.

anonymous
()
Ответ на: комментарий от anonymous

Нет, не одинаковых, а произвольных.

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

минус типы

в общем то возможно так:

data F where
  FV :: ((a == F a) ~ False) => a -> F a
  FL :: [F a] -> F a

Это в 7.8, иначе через фундепы можно выразить только (да и то type level naturals и т.п. не выведутся адекватно).

qnikst ★★★★★
()
Ответ на: комментарий от anonymous

Выводить != угадывать.

Тайпчеккер выводит единственно верный максимально общий тип или говорит, что он этого сделать не может. Единственная особенность, где с натяжкой можно сказать, что это не иак - это defaults в интерпретаторе.

qnikst ★★★★★
()
Ответ на: комментарий от anonymous

Нет.Это (forall a. List a)

Я про Tree из пакета containers. Это рекурсивный ADT вокруг обычных списков позволяющий т.о. делать в них вложенность, то есть такое дерево:

data Tree a = Node {
        rootLabel :: a,         -- ^ label value
        subForest :: Forest a   -- ^ zero or more child trees
    }

type Forest a = [Tree a]

^ кидаем в обычный список subForest Node, в котором свой список и т.д.

Вот на D такое же — http://rosettacode.org/wiki/Flatten_a_list#With_an_Algebraic_Data_Type (там почему-то не видят проблем в том чтобы писать это к «Write a function to flatten the nesting in an arbitrary list of values»), с boost::variants так же будет.

Нет, вложенный список не имеет ничего общего с деревом.

Так ты покажи чего ты хочешь, что тебе не хватает в дереве, что нужна какая-то другая структура данных, что это за структура, чем она отличается, в каких учебниках фигурирует. Концепция общего дерева универсальна среди языков, как и плоского связного списка, нужен flatten из одного в другое — пожалуйста, хоть в Си с грамотным интерфейсом к структурам данных и правильно типизированным flatten в заголовочнике, если ещё с полиморфизмом по типу / ADT, то это начиная с ML и всех прочих языков с дженериками/шаблонами / ADT, то есть в любом случае «проблема» типизации flatten решена ещё в 70-ые годы.

Может не решена проблема типизации магической структуры данных «вложенные списки», но

Ну а вообще сам факт того что написание flatten в статике - тема для докторского диссера, уже показателен.

я ХЗ о чём ты, тебе эту структуру описать на универсальном языке тогда надо (чтобы не «как в лиспе», в лиспе оно один фиг в рантайме на си/java и т.п. прекрасно описано и вся разница в синтаксисе построения, типа «с конструкторами» или без).

anonymous
()
Ответ на: комментарий от anonymous

Ну я поиграл по их правилам немного, сделав структуру как просили. То, что тут две задачи - об этом забыди - одна сделать структуру с требуемыми свойствами, другая сделать flatten. При этом им ещё хочется без «тегов» (конструкторов), которые в лиспе есть под капотом, и там любой тип умеет в typeof :: Lispy -> TypeRep, и cast :: Lispy -> TypeRep -> Lispy, где Lispy это внутреннее представление типов, а TypeRep внутренне описание типа, что уже странно.

qnikst ★★★★★
()
Последнее исправление: qnikst (всего исправлений: 1)
Ответ на: комментарий от x4DA

с конструкторами типов не интересно, я хочу обычный список передать на вход функции.

Обычный список это сахар к конструкторам типов (:) и []. То есть либо мы обсуждаем синтаксический сахар, либо вопрос написания flatten : A -> B и структур данных A и B в статически типизированных языках.

З.Ы. посмотри там ещё на Scala пример, это список-чего-угодно в список-чего-угодно, так как в Scala есть верхний тип и автоматические поднятия типов получается как ты хочешь, то есть синтаксически записи обычного списка и вложенного не отличаются.

anonymous
()
Ответ на: комментарий от qnikst

Тайпчеккер выводит единственно верный

В том и дело, что неверный.

Выводить != угадывать.

Обычно под «выводом» подразумевается получение верного ответа. А когда ответ может получиться верный, а может - неверный, то это странно называть «выводом».

или говорит, что он этого сделать не может.

Ну в том и дело что в данном случае хаскель вместо того чтобы сказать «не могу вывести» сказал «я вывел вот это»

anonymous
()
Ответ на: комментарий от anonymous

Я привёл пример использующий тот же модуль, где оказалось, что type checker вывел верный тип, причём он вывел ровно тот же тип, что и в припере у monk. Как такое может быть?

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

qnikst ★★★★★
()
Последнее исправление: qnikst (всего исправлений: 1)
Ответ на: комментарий от monk

Ты, кстати, заметил, что из-за твоей опечатки твоя программа не собралась, выдав правда «ошибку» в неожиданном для тебя месте. Если ты хочешь привести пример, где TI приводит к проблемам, то ты должен предоставить программу, которая собирается, а результат работы зависит от результатов TI. Т.е. полностью полиморфную программу где результат зависит от типа, прогнать её и сказать, что ожидал другого (но сделать это не элементарно), могу дать подсказку идеи:

import Data.Binary

main = print . encode .(+1) =<< readLn

Результат зависит от того целочисленного типа, что выведется - заставишь скомпиляться подтвердишь свою точку зрения.

qnikst ★★★★★
()
Ответ на: комментарий от monk

Сужу только по SBCL. ML/Haskell знаю только, что они существуют.

turtle_bazon ★★★★★
()
Ответ на: комментарий от x4DA

статическая типизация есть, я говорю что для нее вывода типов нету, то есть толку от такой статики с гулькин нос

Слишком смелое утверждение, с моей точки зрения.

den73 ★★★★★
()
Ответ на: комментарий от tailgunner

Горячая замена кода к типизации отношения не имеет, какой бы хитрой она не была.

Это не вполне очевидно (во всяком случае, мне), поэтому я об этом и написал. Я долго считал, что CL может быть таким динамичным в реализации только благодаря динамической типизации.

В D есть opCall, ЕМНИП

Ну вот. Т.е., из действительно популярных языков только языки хранимых процедур (типа T-SQL) сочетают статическую типизацию и динамизм. Значит, всё-таки их не так просто сочетать.

den73 ★★★★★
()
Ответ на: комментарий от monk

> Что мне скажет тайпчекер, когда я попытаюсь «обновить» функцию, задав ей другую сигнатуру
Потребует обновить также все функции, где используется эта.

Вопрос в том, на какую часть программы и в какой момент распространяется действие тайпчекера. Из практики работы с 2-3 динамическими языками я пришёл к выводу, что это следует делать лениво. Например, при первом вызове.

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

den73 ★★★★★
()
Ответ на: комментарий от x4DA

как ты на горячую заменишь функцию, у которой сигнатура изменилась?

Только что уже ответил, но ещё добавлю. Как правило, сигнатура функции не меняется, а расширяется, путём добавления параметров-ключей (то же, что опции команд в линуксе). Во всяком случае, я так делаю и нахожу это крайне удобным. Если же сигнатура поменялась, то лисп ничего не делает. Вызовы этой функции, которые уже на стеке, продолжают выполнение, не мешая компиляции. При попытке вызова функции со старой сигнатурой попадём в дебаггер. При таком подходе не возникает клинч, если функции взаимно рекурсивны (такой клинч очень мешает разработке в Firebird, а в MS SQL его нет). И в то же время не нарываемся на undefined consequences.

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

Правда, при такой стратегии есть накладные расходы. Но можно переложить их, например, на память, чтобы не падало быстродействие.

Основной «накладной расход» - в конкретный момент программа может быть некорректной по типам.

den73 ★★★★★
()
Ответ на: комментарий от qnikst

Я привёл пример использующий тот же модуль, где оказалось, что type checker вывел верный тип, причём он вывел ровно тот же тип, что и в припере у monk. Как такое может быть?

Ну так тип был другой. Тип же не зависит от _кода_ функции. По-этому ф-я с одним и тем же телом может иметь разные типы. И в зависимости от того, какой она имеет тип, ее тело либо будет либо не будет правильным.

Смотри, у нас статическая типизация работает как? Если чекер МОЖЕТ вывести корректность - программа считается корректной. Если чекер НЕ МОЖЕТ вывести корректность, то программа отвергается и считается некорректной. При этом во втором случае программа вполне может быть правильной на каких-то конкретных примерах. Но чекер допускает программу только тогда, когда она правильна ВСЕГДА.

А вот в данном случае он, почему-то, нарушает эту логику работы и допускает прогармму, которая может правильна, а может - нет.

anonymous
()
Ответ на: комментарий от qnikst

Если ты хочешь привести пример, где TI приводит к проблемам, то ты должен предоставить программу, которая собирается, а результат работы зависит от результатов TI.

С чего вдруг-то? Проблема как раз в том и состоит, что выводится плохое сообщение об ошибке. Зачем программе собираться и давать какие-то результаты работы, если мы говорим именно о выводе ошибок тайпчекера?

Или по-твоему если чекер просто будет выблевывать «где-то в вашей программе есть ошибка» без указания какой-либо конкретики, то это тоже НЕ проблема?

anonymous
()
Ответ на: комментарий от turtle_bazon

Ты это как программист говоришь или просто так?

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

anonymous
()
Ответ на: комментарий от den73

Тогда и функции, где используется эта, понадобится обновить не сразу, а позже.

Проблема в том, что это уже НЕ статическая типизация. Нечто другое.

Если потребовать перехода из корректного состояния программы по отношению к типам сразу в новое корректное, то легко возникнет клинч.

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

anonymous
()
Ответ на: комментарий от anonymous

Но время печати кода - это проценты от общих трудозатрат программиста.

Интересно было бы посмотреть на раскладку трудозатрат программиста по времени.

Я к тому, что при разработке в образе (как принято в CL) проектирование осуществляется написанием кода, тестирование тоже и, как следствие, написание кода занимает почти 100% времени.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Чеккер вывел правильное сообщение об ошибке именно в том месте, где она должна быть. Но это оказалось неожиданным для программиста в силу его некомпетентности.

Ещё раз, ошибки не было, чеккер вывел все абсолютно верно.

По моему нужно чтобы люди использующие инструмент хотя бы поверхностно знали принципы его работы.

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

Тип был тот же!

В первом случае тип был String -> Int, во втором - a -> String -> Int. В какой реальности это один и тот же тип?

anonymous
()
Ответ на: комментарий от monk

Я к тому, что при разработке в образе (как принято в CL) проектирование осуществляется написанием кода, тестирование тоже и, как следствие, написание кода занимает почти 100% времени.

Ну проектирование написанием кода осуществляться не может. В любом случае 90% программист думает и читает код и только в 10% - все остальное. И это оценка сверху явно.

anonymous
()
Ответ на: комментарий от qnikst

Чеккер вывел правильное сообщение об ошибке именно в том месте, где она должна быть.

Нет. ошибка была в том, что не монк опечатлся и не поставил х. То есть тайпчекер даже МОДУЛЬ с ошибкой указал неверно.

Ещё раз, ошибки не было, чеккер вывел все абсолютно верно.

Еще раз, тип функции был String -> Int, тайпчекер вывел a -> String -> Int. То есть тайпчекер вывел НЕВЕРНЫЙ ТИП.

По моему нужно чтобы люди использующие инструмент хотя бы поверхностно знали принципы его работы.

Я прекрасно знаю принципы работы и сам писал чекеры.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.