Io Language: Система сообщений
Сегодня продолжим цикл статей, начатый достопочтенным semka. Поговорим о сообщениях.
В Ио нет вызовов функций, но есть посылка сообщений. У сообщения могут быть аргументы (почти как аргументы функции), но аргументы сообщений не выполняются перед посылкой.
Сообщение может содержать аргументы:
Результату посылки сообщения тоже можно послать сообщение:
Иными словами, конструкция
Когда объект получает сообщение, он ищет слот с именем этого сообщения (в нашем примере
Активация слота не происходит при вызове метода getSlot(slotName) (разумеется, сам слот "getSlot" активируется, а вот slotName — уже нет). Поэтому, если вы хотите получить метод или блок as-is, не вызывая его, то пользуйтесь getSlot(methodName) .
С активацией связан один подводный камень, о котором пойдет речь в конце статьи.
Рассмотрим пример:
Этот код ничего не выведет потому что аргумент "Hello!" println не был выполнен.
Этот код выполнит первый аргумент и выведет на экран Hello! . Недостающие аргументы будут равны nil.
Еще один пример:
Этот код напечатает 12, но не 1234.
Самые догадливые уже поняли, что выполняются только объявленные аргументы. Самое забавное — это то, как они выполняются, где и куда можно запустить свои грязные руки. И зачем все это нужно.
Когда Ио активирует слот (сиречь вызывает метод), он создает специальный объект Locals. Это довольно забавный объект, имеющий ряд важных особенностей. Во-первых, прототип этого объекта — self, т.е. указатель на тот объект, которому послано сообщение. Таким образом, мы можем создавать локальные слоты (aka локальные переменные), не вмешиваясь в объект-получатель, а также получить доступ ко всем слотам этого объекта. Во-вторых, в этом объекте есть как минимум три слота: self (указывает на объект-получатель), call (содержит массу информации о вызове) и updateSlot. Последний отличается от обычного updateSlot тем, что обновляет слот не в locals, а в объекте-получателе. Это сделано исключительно удобства ради, чтобы писать a = b вместо self a = b .
Самое замечательное в объекте call — это несколько слотов:
call evalArgAt(argNumber) и evalArgs выполняют некоторый или все аргументы в контексте сендера (call sender). Но никто не мешает сделать что-нибудь такое:
На экране мы увидим:
Иными словами, мы можем манипулировать кодом как нам угодно: передавать, хранить, выполнять в произвольных контекстах. Можно посмотреть код уже созданного объекта: Coroutine getSlot("yield") code вернет код метода yield в виде строки:
Кстати, про прототип locals я приврал. Если вы еще помните, кроме методов в Ио есть блоки (замыкания). Их единственное отличие от методов состоит в том, что прототип Locals у блоков не указатель на получатель сообщения, а указатель на scope, т.е. тот объект, в котором блок был создан (и на который он замкнут). Чтобы у вас окончательно уехала крыша, добавлю, что слот блока scope в точности равен call sender в момент вызова метода block .
Предположим, вы пишите метод, который может принимать другой метод в качестве аргумента:
Если вы напишите func внутри тела метода, то вы неминуемо вызовете переданный метод. Если вам нужно просто получить его значение, то вам придется каждый раз обращаться к нему через getSlot("func") .
Самое простое: Message setCachedResult(res) устанавливает вычисленное значение сообщения, которое будет возвращаться при попытке послать это сообщение куда-либо. Более сложный вариант: кешировать специальную логику, адаптированную под конкретное сообщение в данной точке вызова.
Например, у метода List map есть три реализации: с одним, двумя и тремя аргументами. В текущей реализации проверка количества аргументов происходит при каждом вызове, хотя возможно закешировать нужный вариант метода в call message .
На основе этой функциональности возможно построение более сложных схем оптимизации.
В Ио нет вызовов функций, но есть посылка сообщений. У сообщения могут быть аргументы (почти как аргументы функции), но аргументы сообщений не выполняются перед посылкой.
Как посылают сообщения
Посылка сообщения выглядит как "объект", "пробел", "сообщение":
Database connect
Сообщение может содержать аргументы:
Database findByName("Oleg")
Результату посылки сообщения тоже можно послать сообщение:
Database findByName("Oleg") lastName # возможно, вернет "Andreev"
Иными словами, конструкция
a b c d эквивалентна a().b().c().d() в каком-нибудь джава-подобном синтаксисе.Как выполняются сообщения
Для начала запомните, что аргументы сообщения не выполняются. Если было написаноDatabase connect(blah-blah), то blah-blah не будет выполнено перед посылкой connect, а будет передано как часть сообщения "connect(blah-blah)" (да-да, словами). Когда объект получает сообщение, он ищет слот с именем этого сообщения (в нашем примере
connect). Если слот не найден (как обычно и происходит), его поиск осуществляется рекурсивно во всех прототипах объекта и их прототипах. Если нужный слот нигде не найден, то запускается поиск слота forward (аналог method_missing в Руби). Как только какой-нибудь подходящий слот найден, его значение активируется (activation). (Примечание: в каком же именно объекте лежит найденный слот вам расскажет Object contextWithSlot(slotName) .)Активация
Для обычных значений активация ничего не делает, в просто возвращает это самое значение. Таким образом, активация обычных слотов, хранящих числа, строки или многие другие объекты, ничем не отличается от получения значения через getSlot(slotName) . А вот те объекты, которые помечены как activatable, вызывают метод activate. По-умолчанию, только два объекта активируемые: Block (блоки и методы) и CFunction (линки к сишным функциям). Все остальные объекты можно сделать активируемыми с помощью setIsActivatable(true) .Активация слота не происходит при вызове метода getSlot(slotName) (разумеется, сам слот "getSlot" активируется, а вот slotName — уже нет). Поэтому, если вы хотите получить метод или блок as-is, не вызывая его, то пользуйтесь getSlot(methodName) .
С активацией связан один подводный камень, о котором пойдет речь в конце статьи.
Эээ. А когда аргументы выполняются-то?
Сообщение послано, слот найден и активирован. Но когда и в каком контексте будут вычислены аргументы сообщения?Рассмотрим пример:
withoutArgs := method() # метод возвращает nil
withoutArgs("Hello!" println)
Этот код ничего не выведет потому что аргумент "Hello!" println не был выполнен.
withArgs := method(a, b, c, list(a,b,c)) # метод возвращает список аргументов
withArgs("Hello!" println) # возвращает list("Hello!", nil, nil)
Этот код выполнит первый аргумент и выведет на экран Hello! . Недостающие аргументы будут равны nil.
Еще один пример:
withTwoArgs := method(a, b, nil)
withTwoArgs(1 print, 2 print, 3 print, 4 print)
Этот код напечатает 12, но не 1234.
Самые догадливые уже поняли, что выполняются только объявленные аргументы. Самое забавное — это то, как они выполняются, где и куда можно запустить свои грязные руки. И зачем все это нужно.
Интроспекция вызова метода
Вы еще помните, что в Ио есть только объекты, прототипы, слоты и сообщения? Глобальных и локальных переменных в этом списке нет.Когда Ио активирует слот (сиречь вызывает метод), он создает специальный объект Locals. Это довольно забавный объект, имеющий ряд важных особенностей. Во-первых, прототип этого объекта — self, т.е. указатель на тот объект, которому послано сообщение. Таким образом, мы можем создавать локальные слоты (aka локальные переменные), не вмешиваясь в объект-получатель, а также получить доступ ко всем слотам этого объекта. Во-вторых, в этом объекте есть как минимум три слота: self (указывает на объект-получатель), call (содержит массу информации о вызове) и updateSlot. Последний отличается от обычного updateSlot тем, что обновляет слот не в locals, а в объекте-получателе. Это сделано исключительно удобства ради, чтобы писать a = b вместо self a = b .
Самое замечательное в объекте call — это несколько слотов:
- call sender — объект (locals), в котором было послано сообщение.
- call target — объект, которому было послано сообщение (== self).
- call message — собственно, сообщение, содержащее имя и аргументы.
call evalArgAt(argNumber) и evalArgs выполняют некоторый или все аргументы в контексте сендера (call sender). Но никто не мешает сделать что-нибудь такое:
Object do := method(call message arguments first doInContext(self) )
"string" do(
type println
size println
encoding println
)
На экране мы увидим:
Sequence
6
ascii
Иными словами, мы можем манипулировать кодом как нам угодно: передавать, хранить, выполнять в произвольных контекстах. Можно посмотреть код уже созданного объекта: Coroutine getSlot("yield") code вернет код метода yield в виде строки:
method(
if(yieldingCoros isEmpty, return )
yieldingCoros append(self)
next := yieldingCoros removeFirst
if(next == self, return )
if(next, next resume)
)
Кстати, про прототип locals я приврал. Если вы еще помните, кроме методов в Ио есть блоки (замыкания). Их единственное отличие от методов состоит в том, что прототип Locals у блоков не указатель на получатель сообщения, а указатель на scope, т.е. тот объект, в котором блок был создан (и на который он замкнут). Чтобы у вас окончательно уехала крыша, добавлю, что слот блока scope в точности равен call sender в момент вызова метода block .
Обратно к локальным переменным
Обладая столь мощным оружием, как call sender и call message , мы можем описать вызов метода и выполнение аргументов на языке Ио. Действительно, говоря method(a, b, nil) , мы сообщаем, что хотим создавать слоты "a" и "b" в locals и заполнять их значениями, которые получены после выполнения соответствующих аргументов в контексте call sender. Домашнее задание: написать метод method2 , который создает методы так же, как и встроенный method и проделывает все необходимые манипуляции с doInContext для объявленных и переданных аргументов.Обещанная уловка с активацией
В джава-подобном синтаксисе доступ к функции и её вызов выглядят так: obj.func и obj.func() . В Ио получение значения без активации возможно через obj getSlot("slot") , когда obj slot обязательно активирует значение слота.Предположим, вы пишите метод, который может принимать другой метод в качестве аргумента:
method(func, ...)
Если вы напишите func внутри тела метода, то вы неминуемо вызовете переданный метод. Если вам нужно просто получить его значение, то вам придется каждый раз обращаться к нему через getSlot("func") .
Конструирование сообщений
Метод message возвращает свой первый аргумент как невыполненное сообщение:
msg := message(something(arg1, arg2, arg3) nextMessage(arg1, arg2) more)
msg name # => "something"
msg next # => message(nextMessage(arg1, arg2) more)
msg next next # => message(more)
msg next name # => "nextMessage"
msg arguments # => list(message(arg1), message(arg2), message(arg3))
Резюме
Теперь вы знаете как происходит посылка сообщений, вызов методов и выполнение аргументов. Надеюсь, вам стали понятны конструкции типа list(1,2,3) foreach(print) (print посылается каждому элементу списка) или list(1,2,3) map(*2) (каждому элементу посылается *(2) при создании копии списка).Bonus track: call sites
Полтора землекопа, которые дочитали до конца статьи могут быть вознаграждены особо восхитительной информацией. В концепции Ио возможно достучаться то так называемой "точки вызова". Это не колстек, не контекст aka "call sender", а именно точка кода, в которой произошел вызов. Все дело в том, что call message всегда возвращает один и тот же объект, если он не сгенерирован на лету, а описан в коде (ну, или сгенерирован один раз и все время посылается). Что это дает? Поскольку один метод может обрабатывать разнообразные сообщения (в разных местах программы), становится возможным закешировать что-нибудь полезное в релевантных местах.Самое простое: Message setCachedResult(res) устанавливает вычисленное значение сообщения, которое будет возвращаться при попытке послать это сообщение куда-либо. Более сложный вариант: кешировать специальную логику, адаптированную под конкретное сообщение в данной точке вызова.
Например, у метода List map есть три реализации: с одним, двумя и тремя аргументами. В текущей реализации проверка количества аргументов происходит при каждом вызове, хотя возможно закешировать нужный вариант метода в call message .
На основе этой функциональности возможно построение более сложных схем оптимизации.



комментарии (20)