Pull to refresh

Io Language: Система сообщений

Reading time 5 min
Views 1.9K
Сегодня продолжим цикл статей, начатый достопочтенным semka. Поговорим о сообщениях.

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


Как посылают сообщения

Посылка сообщения выглядит как «объект», «пробел», «сообщение»:
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.
    На основе этой функциональности возможно построение более сложных схем оптимизации.
Tags:
Hubs:
+27
Comments 20
Comments Comments 20

Articles