Подтвердить что ты не робот

Почему Clojure различает символы и vars?

Я уже видел этот вопрос, но это не объясняет, о чем мне интересно.

Когда я впервые пришел к Clojure из Common Lisp, я был озадачен, почему он рассматривает символы и ключевые слова как отдельные типы, но позже я понял это, и теперь я думаю, что это замечательная идея. Теперь я пытаюсь разобраться, почему символы и vars являются отдельными объектами.

Насколько я знаю, реализации Common Lisp обычно представляют собой "символ" , используя структуру, которая имеет 1) строку для имени, 2) указатель на значение символа при оценке в позиции вызова функции, 3) a указатель на его значение при оценке вне позиции вызова и 4) список свойств и т.д.

Игнорируя различие Lisp -1/Lisp -2, факт остается фактом: в CL объект "символ" указывает прямо на его значение. Другими словами, CL объединяет то, что Clojure вызывает "символ" и "var" в одном объекте.

В Clojure, чтобы оценить символ, сначала нужно найти соответствующий var, тогда var должен быть разыменован. Почему Clojure работает таким образом? Какая польза от такого дизайна может быть? Я понимаю, что у vars есть определенные специальные свойства (они могут быть частными или const или динамическими...), но не могли ли эти свойства просто применяться к самому символу?

4b9b3361

Ответ 1

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

1: Разделив понятия "переменная" и "идентификатор, который может ссылаться на переменную", Clojure делает вещи более понятными концептуально. В CL, когда читатель видит a, он возвращает объект символа, который переносит указатели на привязки верхнего уровня, даже если a локально связан в текущей области. (В этом случае оценщик не будет использовать эти привязки верхнего уровня.) В Clojure символ - это просто идентификатор, не более того.

Это связано с тем, что сделаны некоторые плакаты, которые также могут ссылаться на классы Java в Clojure. Если символы несут привязки к ним, эти привязки можно просто игнорировать в контекстах, где символ относится к классу Java, но это было бы грязно концептуально.

2: В некоторых случаях люди могут использовать символы в качестве ключей карты и т.д. Если символы являются изменяемыми объектами (как они есть в CL), они не будут хорошо соответствовать Clojure неизменяемым структурам данных.

3: В (возможно, редких) случаях, когда символы используются в качестве ключей карты и т.д. и, возможно, даже возвращены API, семантика равенства символов Clojure более интуитивно понятна, чем символы CL. (См. Ответ @amalloy.)

4: Поскольку Clojure подчеркивает функциональное программирование, большая часть работы выполняется с использованием функций более высокого порядка, таких как partial, comp, juxt и т.д. Даже если вы не используете их, вы все равно можете использовать функции в качестве аргументов для своих собственных функций и т.д.

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

Даже в таких ситуациях, используя #'my-func, вы можете явно запросить, чтобы текущее значение my-func проверялось каждый раз, когда вызывается производная функция. (Предположительно ценой небольшого удара производительности.)

В CL или Scheme, если мне нужен такой тип косвенности, я могу представить, как хранить объект функции в cons или vector или struct, и получать его оттуда каждый раз, когда он должен был быть вызван. Фактически, в любой момент, когда мне нужен объект "изменчивой ссылки", который может быть разделен между различными частями кода, я мог бы просто использовать минус или другую изменяемую структуру. Но в Clojure, списки/векторы/и т.д. все неизменяемы, так что вам нужно каким-то образом явно ссылаться на "нечто изменяемое".

Ответ 2

Другие вопросы затронули многие истинные аспекты символов, но я попытаюсь объяснить это под другим углом.

Символы - это имена

В отличие от большинства языков программирования, Clojure проводит различие между вещами и именами вещей. В большинстве языков, если я говорю что-то вроде var x = 1, тогда правильно и полно сказать "x is 1" или "значение x равно 1". Но в Clojure, если я говорю (def x 1), я сделал две вещи: я создал Var (объект, сохраняющий значение), и я назвал его символом x. Говоря "значение x равно 1", не совсем рассказать всю историю в Clojure. Более точный (хотя и громоздкий) оператор будет "значением переменной var, обозначенной символом x равен 1".

Символы сами по себе являются просто именами, в то время как vars являются несущими ценности объектами и сами не имеют имен. Если расширить предыдущий пример и сказать (def y x), я не создал новый var, я только что присвоил существующему var второе имя. Два символа x и y являются именами для одного и того же var, который имеет значение 1.

Аналогия: меня зовут "Люк", но это не то же самое со мной, с кем я - как человек. Это просто слово. Не исключено, что в какой-то момент я мог бы изменить свое имя, и есть много других людей, которые разделяют мое имя. Но в контексте моего круга друзей (в моем пространстве имен, если хотите) слово "Люк" означает меня. И в фантазии Clojure -land, я мог бы быть var, несущей ценность для вас.

Но почему?

Итак, почему эта дополнительная концепция имен в отличие от переменных, а не объединение этих двух языков в большинстве языков?

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

Самое главное, однако, это часть философии Clojure "код - это данные". Строка кода (def x 1) - это не просто выражение, это также данные, в частности список, состоящий из значений def, x и 1. Это очень важно, особенно для макросов, которые управляют кодом как данные.

Но если (def x 1) - список, чем значения в списке? И в частности, каковы типы этих ценностей? Очевидно, что 1 - число. Но как насчет def и x? Каков их тип, когда я манипулирую ими как данные? Ответ, конечно, символы.

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

Ответ 3

(ns a)

(defn foo [] 'foo)
(prn (foo))


(ns b)

(defn foo [] 'foo))
(prn (foo))

Символ foo является тем же самым символом в обоих контекстах (т.е. (= 'foo (a/foo) (b/foo)) является истинным), но в двух контекстах он должен иметь другое значение (в данном случае указатель на одну из двух функций).

Ответ 4

Я предположил следующий вопрос из вашего поста (скажите мне, если я не в базе):
Почему существуют два уровня косвенности для отображения символов в их базовые значения?

Когда я впервые обратился к этому вопросу, через некоторое время я придумал две возможные причины: "переосмыслить" на лету "и связанное с ней понятие динамическая область. Однако следующее убеждало меня, что ни одна из этих причин не является причиной такого двойного направления:

=> (identical? (def a 0) (def a 10))
=> true

=> (declare ^:dynamic bar)
=> (binding [bar "bar1"]
     (identical? (var bar)
                 (binding [bar "bar2"]
                   (var bar))))
=> true

Для меня это показывает, что ни "переизбытка", ни динамическая область не производят никакого изменения отношения между символом пространства имен и символом var, на который он указывает.

На этом этапе я задам новый вопрос:
Является ли символ, ограниченный именами, всегда синонимом символа var, на который он ссылается?

Если ответ на этот вопрос да, то я просто не понимаю, почему должен быть еще один уровень косвенности.

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

Полагаю, в заключение, большой вопрос: P

Ответ 5

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

В качестве конкретного примера символы могут быть счастливыми до создания var, которые они ссылаются:

(def my-code `(foo 1 2))     ;; create a list containing symbol user/foo
=> #'user/my-code

my-code                      ;; confirm my-code contains the symbol user/foo
=> (user/foo 1 2)

(eval my-code)               ;; fails because user/foo not bound to a var
=> CompilerException java.lang.RuntimeException: No such var: user/foo...

(def foo +)                  ;; define user/foo
=> #'user/foo

(eval my-code)               ;; now it works!
=> 3

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

Ответ 6

для Common Lisp или другого lisp, пожалуйста, проверьте: Differences with other Lisps from http://clojure.org/lisps

Ответ 7

Clojure - это мой первый (и только) lisp на сегодняшний день, поэтому этот ответ является чем-то вроде догадки. Тем не менее, следующая дискуссия с веб-сайта clojure кажется уместным (акцент мой):

Clojure - это практический язык, который признает случайную необходимость поддерживать постоянную ссылку на меняющуюся стоимость и обеспечивает 4 различных механизма для этого контролируемым образом - Vars, Refs, Agents and Atoms. Vars предоставляет механизм для ссылки на изменяемое место хранения, которое может динамически восстанавливаться (в новое хранилище) на основе для каждой строки. Каждый Var может (но не обязательно) иметь корневую привязку, которая является привязкой, которая разделяется всеми потоками, которые не имеют привязки для каждой нити. Таким образом, значение Var является значением его привязки для каждой нити или, если оно не связано в потоке, запрашивающем значение, значение корневой привязки, если оно есть.

Таким образом, косвенные символы для Vars разрешают поточное безопасное динамическое повторное связывание (возможно, это можно сделать другими способами, но я не знаю). Я считаю, что это часть основной clojure философии строго и широко различающей идентичности и состояния, чтобы обеспечить надежный concurrency.

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

Ответ 8

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

Этот парень объясняет это намного лучше меня: https://www.youtube.com/watch?v=8NUI07y1SlQ (по общему признанию, почти через два года после публикации этого вопроса). Он также обсуждает некоторые последствия производительности и дает пример, когда это дополнительное косвенное отношение стоит около 10% производительности. Это не так уж плохо, учитывая то, что вы получаете за это. Самое большое наказание, хотя и возникает в виде дополнительного использования кучи и длительного времени запуска Clojure, которое по-прежнему является большой проблемой, я думаю.