В последние несколько дней на Хабре был опубликован ряд статей, общим лейтмотивом которых (особенно в комментариях) стало противостояние тупоконечников с остроконечниками – адепты ФП против ООП, хотя их и призывали не спорить. Иногда обсуждали Erlang, в связи с чем мне вспомнился короткий пост на тему от Джо Армстронга, одного из создателей этого языка, написанный им в конце 2018 года на форуме по Elixir в ответ на вопрос о парадигме языка. Думаю, его комментарий будет интересен.


Джо Армстронг, 12 сентября 2018 года


Всё хорошее в Elixir (и Erlang) связано с параллелизмом – простое создание независимых процессов и обмен сообщениями. И именно это является самой сущностью объектно-ориентированного программирования, на что неоднократно указывал Алан Кей.


ООП полностью посвящено объектам. Объекты отвечают (или должны отвечать) на сообщения, и когда вы хотите что-то сделать – вы посылаете объекту сообщение, а уж как он обработает его — совершенно не важно. Думайте об объектах как о "черных ящиках", и когда вы что-то хотите от них — просто посылайте сообщения, а они вам будут слать сообщения в ответ.


Как всё устроено внутри не имеет значения – будет ли код внутри "черного ящика" функциональным или императивным – важно лишь то, что именно он должен делать.


К сожалению, хоть первый успешный объектно-ориентированный язык на основе этой модели (Smalltalk) и оперировал понятиями "объект" и "сообщение", но последние в Smalltalk были не настоящими сообщениями, а лишь замаскированными синхронными вызовами функций. Эта же ошибка была повторена в С++, потом в Java, и главная идея ООП выродилась в странную парадигму организации кода по классам и методам.


Erlang и Elixir обеспечивают легкое создание миллионов изолированных процессов, где всё работает через посылку сообщений между ними. Архитектура системы определяется тем уровнем параллелизма, который вы желаете, с последующим отображением его на процессы непосредственно.


Web-сервер на Elixir для 10 000 пользователей это не "один web-сервер с 10 000 пользователей" (как в случае с Apache или Jigsaw и им подобным), но это "10 000 web-серверов на каждого пользователя" — согласитесь, радикальный отход от традиционной модели.


Тот факт, что для описания модели процессов Erlang/Elixir был использован простой функциональный язык – почти случайность. Всё началось с системы логического программирования (Prolog), а такие вещи, как, например, C-node, могут быть написаны вообще на любом языке. По-настоящему важным для Elixir (и любого другого BEAM-языка) является способность их виртуальной машины оперировать экстремально большим количеством параллельных процессов.


Долгое время я говорил, что "Erlang это единственный настоящий объектно-ориентированный язык". Наверное, теперь я могу добавить к нему и Elixir.


Для ООП базовыми вещами являются:


  • изоляция между объектами (у нас это есть)
  • позднее связывание (мы решаем что делать только тогда, когда процесс получит сообщение)
  • полиморфизм (все объекты могут отвечать на сообщение одного типа, например, "print-yourself" и любой объект может знать, как это выполнить)

Гораздо менее важно:


  • разделение на классы и методы
  • синтаксис
  • программная модель (функциональная или императивная)

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


То, что Erlang (и Elixir) привнес в программирование, было идеей связей (link – прим. переведчика). Изначально предложенная Майком Вильямсом, она заключается в расширении возможности обработки ошибок, позволяя её делать за границами процессов. Имея это мы получили все необходимые инструменты для построения деревьев супервизоров и т.п.


Супервизоры, gen_server'ы и всё такое прочее – это всего-навсего библиотеки, которые скрывают от пользователя некоторые подробности. Простые внутри, написанные с помощью тех же инструментов – параллельных процессов и связей между ними.


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


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


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


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


Различие между Erlang и Elixir и «всеми остальными» заключается в механизмах обеспечения параллелизма и отказоустойчивости, и речь тут не о монадах, синтаксисе, или "чистоте" ФП.


Теперь вопрос – хотите ли вы обработать 10 000 пользователей в одном потоке, используя коллбеки для эмуляции параллелизма, или вы все же хотите создать 10 000 параллельных процессов, каждый из которых прост и не нуждается в коллбеках вовсе?


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


Я думаю, что большая проблема популяризации Erlang/Elixir состоит в том, что вам нужно объяснить, как большое количество параллельных процессов помогает решить вашу конкретную проблему. Поскольку никакие другие распространенные языки изначально не нацелены на параллельное программирование и не облегчают его сколь-нибудь значимым образом, необходимость в нём для людей не до конца осознана и понятна.


"Но я могу делать всё на коллбеках в одном потоке" – скажут они. И они это делают, и это мучительно сложно. И вы спрашиваете "что произойдет, если коллбек попадет в цикл или вызовет исключение?", и если они не понимают вопроса – то тут вы должны потрудиться и объяснить проблему. Но если вопрос окажется понятен – то расскажите им, что есть одна далёкая страна, в которой для программирования параллелизма коллбеки не нужны.


Кажется, всё вышесказанное можно сократить до следующего: пожалуйста, не рекламируйте Elixir как язык функционального программирования – он им не является. Он язык параллельного программирования (CPL, Concurrent Programming Language).


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


" — Что такое CPL?"
" — Ты знаешь, это то, на чем сделан WhatsApp ..."


От переводчика


Прежде всего, хотелось бы выразить сожаление в связи со скоропостижной смертью Джо Армстронга — он скончался 20 апреля 2019 года. Недооценить его вклад в развитие индустрии сложно, равно как и его талант популяризатора — книги, выступления, активная деятельность в сообществах Erlang и Elixir.


Полезные ссылки:


Комментарии (59)


  1. epishman
    04.05.2019 18:11
    -7

    Суть спора между ООП и ФП — это какими должны быть отношения между данными, и логикой их обработки. ООП говорит, что данные и логику нужно объединять в одну сущность (класс), ФП говорит что это 2 независимых сущности. Когда у вас распределенная система — вам придется логику помещать внутрь сообщения, а скорее всего еще научиться ее (де)сериализовывать. То есть использовать машинный код практически невозможно, а значит привет виртуальная машина, сборка мусора и проч.


    1. vassabi
      06.05.2019 19:16

      у ФП тоже нет разделения — в нем есть только функции, а все «данные» — это параметры на входе в функцию и результаты на выходе из функции.


  1. EvgeniiR
    04.05.2019 20:49

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


    1. akryukov
      04.05.2019 21:56
      +1

      Понимаете, какая штука. Существует два понятия ООП. Одно — "общепринятое ООП" и другое — "ООП в Smalltalk", которое существует только в контексте истории и каких-нибудь специфических языков.
      Термины людям нужны для однозначного общения друг с другом. Пока основная масса людей под сокращенным термином "ООП" понимает именно общепринятую версию с инкапсуляцией, наследованием и полиморфизмом, то ни о каком "искажении" речи быть не может. Пока основные промышленные языки используют И.Н.П., пока на собеседованиях у джунов спрашивают про ООП и считают ответ про И.Н.П. верным, все останется на своих местах.


      У Erlang же вроде не ООП, а модель акторов. Зачем отвоевывать занятый термин, если существует свой уникальный?


      1. Source
        04.05.2019 23:03
        +2

        Ну, давайте посмотрим на определение общепринятого ООП:


        Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data, in the form of fields (often known as attributes), and code, in the form of procedures (often known as methods). A feature of objects is an object's procedures that can access and often modify the data fields of the object with which they are associated (objects have a notion of "this" or "self"). In OOP, computer programs are designed by making them out of objects that interact with one another.

        Как видите, никакого противоречия с "ООП в Smalltalk" тут нет.
        Не знаю уж, кто первым придумал ассоциировать ООП с И.Н.П. и рассказывать студентам про 3 китов. Но этот человек явно заслужил премию "почётный диверсант" за введение в заблуждение огромного количества людей. Причём, насколько я замечал, эта дезинформация гораздо популярнее в РуНете.


        1. EvgeniiR
          04.05.2019 23:12

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

          Спасибо, это очень удачная фраза для описания тех мыслей что меня иногда посещают )

          А по поводу рунета — я смотрю на эту статью — medium.freecodecamp.org/object-oriented-programming-concepts-21bb035f7260, и вобщем то на много других с медиума, и там всё так же грустно.


        1. akryukov
          04.05.2019 23:45

          Русским студентам все же ближе русская Википедия, в которой написано


          Объе?ктно-ориенти?рованное программи?рование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определённого класса, а классы образуют иерархию наследования[1].

          Это определение подразумевает И.Н.П. и ссылается на книгу Грэди Буч. Объектно-ориентированный анализ и проектирование с примерами приложений на С++ = Object-Oriented Analysis and Design with Applications / Пер. И.Романовский, Ф.Андреев. — 2-е изд. — М., СПб.: «Бином», «Невский диалект», 1998. — 560 с. — 6000 экз. — ISBN 5-7989-0067-3.


          Позже посмотрю как у него в оригинале даётся определение ООП. Может скопировали не так, может переводчики исказили. Ну а может Грабли Буч и есть тот диверсант. =)


          1. EvgeniiR
            04.05.2019 23:53
            +1

            Русским студентам все же ближе русская Википедия, в которой написан

            Русским(и не русским) студентам ближе заучить первое попавшееся определение ради галочки в своём портфолио, и чтобы на зачете было что рассказать, а не вникать что же это такое и зачем нужно Ж)
            Ну а может Грабли Буч и есть тот диверсант. =)

            Мне пока кажется, что понятие просто искажалось и сужалось с новых к привычным вещам постепенно. Но Буч вещал именно об искаженном понятии. Его определение из 3-его издания книги:
            Object-oriented programming is a method of implementation in which programs
            are organized as cooperative collections of objects, each of which represents an
            instance of some class, and whose classes are all members of a hierarchy of
            classes united via inheritance relationships.

            Он же, кстати, внёс свой вклад(написал книгу) в популяризацию UML, что, по сути, выставление классов и их иерархий во главу угла.

            Ну и ещё, свой вклад в сие неблагородное дело явно внёс Страуструп, называя свой язык ОО, и плотной ассоциации идеологии классов и стандартных «трёх китов»(+ абстракцию) с данным понятием.
            What is ‘‘Object-Oriented Programming’’? (1991 revised version), Bjarne Stroustrup — www.stroustrup.com/whatis.pdf

            The general idea is to equate ‘‘support for data abstraction’’ with the ability to define and use new types and equate ‘‘support for object-oriented programming’’ with the ability to express type hierarchies.

            The basic support a programmer needs to write object-oriented programs consists of a class mechanism with inheritance and a mechanism that allows calls of member functions to depend on the actual type of an object

            И, вобщем, там ещё 20 страниц такого описания. Кстати, со ссылками в конце, в том числе на некоторые источники с информацией 1986 года об ООП.


            1. Source
              06.05.2019 01:36

              Ну и ещё, свой вклад в сие неблагородное дело явно внёс Страуструп, называя свой язык ОО, и плотной ассоциации идеологии классов и стандартных «трёх китов»(+ абстракцию) с данным понятием.

              Кстати, вполне возможно, что он и есть тот самый "диверсант", поскольку он начал позиционировать C with classes как объектно-ориентированный язык, коим он не являлся. А учитывая популярность C, ему удалось, как сейчас говорят, хайпануть на этой теме и завести IT-индустрию в многолетние мучительные поиски паттернов и антипаттернов, чтобы хоть как-то этим безобразием можно было пользоваться. А с приходом мультиядерных процессоров опять всё сломалось… Вдруг выяснилось, что shared data — это больно и вокруг этого нагородили ещё кучу костылей.


              На фоне всего этого, Армстронг и вся команда разработчиков Erlang — просто гении, обогнавшие развитие индустрии на десятилетия.


          1. Source
            06.05.2019 00:03
            +2

            В принципе, любое определение ООП, в котором есть слово "класс", уже неверно. Хотя бы потому что классы — это все-лишь один из способов реализации ООП. Соответственно все такие определения нарушают open-closed principle xD


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


            Как видно, модель акторов, классы, прототипы и т.д. — всё это просто разные реализации ООП. И ООП — это только то, что их объединяет.


      1. EvgeniiR
        04.05.2019 23:07
        +1

        Понимаете, какая штука. Существует два понятия ООП. Одно — «общепринятое ООП» и другое — «ООП в Smalltalk», которое существует только в контексте истории и каких-нибудь специфических языков.
        Термины людям нужны для однозначного общения друг с другом. Пока основная масса людей под сокращенным термином «ООП» понимает именно общепринятую версию с инкапсуляцией, наследованием и полиморфизмом, то ни о каком «искажении» речи быть не может. Пока основные промышленные языки используют И.Н.П., пока на собеседованиях у джунов спрашивают про ООП и считают ответ про И.Н.П. верным, все останется на своих местах.

        Нуу… Я считаю иначе.
        Популярное заблуждение != новый стандарт. Я не смог найти внятного определение «общепринятого ООП», и кто этот термин вообще ввёл. Тем более неясны преимущества такого подхода, и как оно вообще отличается от того что было раньше.
        Почему адепты данного «классического ООП» не называют язык Си объектно-ориентированным? В нем ведь вполне реализуются все 3 этих принципа. В нем есть полиморфизм в виде возможности передачи указателей на функции, в нём есть наследование(да, по этому пункту, одному из 3-х, всё же есть отличия, тут у совеременных «классических» языков есть преимущество), но ведь зато у Си есть преимущество в инкапсуляции. Если под инкапсуляцией мы имеем ввиду складывание данных и функций в одном месте, то Си изначально не даст доступ к данным одного модуля(файла) из другого модуля(файла), пока мы явно не укажем доступ в заголовочном файле.

        Вообщем, ключевой момент, кмк в том, что «классичесое ООП» это не какой-то внятный термин, а лишь плод заблуждений и постепенное, с годами, искажение оригинальной затеи, и смещение фокуса с изначальных идей на более простые и понятные, для ребят что всегда писали процедурный код, вещи. И то что сейчас часто кличут «ООП» по факту теми же процедурами и является(модель = структура с геттерами и сеттерами, логика в процедурах)

        Причем я не считаю что людям срочно всем нужно идти учить, читать, что такое ООП от Алана Кея. Тут большая проблема в том, что людям навязывается ООП. Если кто-то скажет что он не знает что такое ООП — на него посмотрят да скажут «А я думал ты программист!? 0_о», а новичку первым же делом будут советовать — выучи, выучи главное ООП. А он пойдет и выучит, выучит что ООП это инкапсуляция наследование и полиморфизм. Только вот LSP он не выучит, «Composition over Inheritance» он тоже не выучит. Польза то какая от всего этого?
        Для новичков это часто отражает, наконец то, отход от стиля написания кода всей программы в одном файле, или в какой-нибудь main() процедуре. Но в таком случае, то что ему нужно называется скорее Объектно-ориентированный дизайн. Это как раз полезные идеи из ООП, которые можно применять в нашем коде.

        У Erlang же вроде не ООП, а модель акторов. Зачем отвоевывать занятый термин, если существует свой уникальный?

        Да, вы правы, просто модель экторов весьма близка к идеям ООП, по которому Кей даже сокрушался, что стоило бы назвать его message-orientied programming. Идея данных высказываний лишь в противопоставлении. То есть, что Эрланг с его Actor model ближе к идеям ООП чем какая-нибудь Java.


        1. KoCMoHaBT61
          05.05.2019 09:11

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


  1. evocatus
    04.05.2019 21:23

    Кстати, Роберт Мартин постоянно говорит, что ООП не противоречит ФП:
    youtu.be/7Zlp9rKHGD4?t=2984


    1. nickolaym
      05.05.2019 00:13

      Так известно же, что ООП/ФП — это ФП/ООП для бедных.


  1. habamax
    05.05.2019 09:24

    «Concurrency is not parallelism». Не думаю, что Армстронг говорил про параллелизм, а не конкурентность (надо источник посмотреть)


    1. AstarothAst
      05.05.2019 20:58

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


      1. habamax
        05.05.2019 21:03

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


        1. AstarothAst
          06.05.2019 10:11

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


      1. habamax
        06.05.2019 09:45

        Почитал оригинал — используется слово concurrent и никак не parallel, ну а на эту тему делал замечательный доклад Роб Пайк (хоть и в контексте языка Го, но тем не менее).


        Перевод:


        Теперь вопрос – хотите ли вы обработать 10 000 пользователей в одном потоке, используя коллбеки для эмуляции параллелизма, или вы все же хотите создать 10 000 параллельных процессов, каждый из которых прост и не нуждается в коллбеках вовсе?

        Источник:


        Now would you like to handle 10,000 users in a single thread using callbacks to emulate concurrency or would you like to make 10,000 concurrent processes, each of which is simple and has no callbacks.


  1. tumikosha
    05.05.2019 15:42

    «один web-сервер с 10 000 пользователей»
    Которые ничего не делают, а просто подключены! Throughput близок к нулю.
    Это конечно достижение, но очень сомнительное.


    1. AstarothAst
      05.05.2019 21:00

      Что значит «просто подключены»? Они подключены, они посылают сообщения, сообщения обрабатываются и ответ возвращается — и все это может происходить разом на нескольких машинах, что бы обеспечить ту самую пропускную способность.


      1. tumikosha
        05.05.2019 22:20

        >они посылают сообщения
        как часто?
        > сообщения обрабатываются
        Ага, ага. Сами-то пробовали?


        1. azmar
          06.05.2019 01:59

          Пробовали, в эрланге т.н. гринтреды и они так работают


        1. AstarothAst
          06.05.2019 10:09
          +1

          как часто?

          По мере надобности.

          Ага, ага. Сами-то пробовали?

          Да, пробовали.

          В чем, собственно, вопрос-то? В том хватит ли аппаратной мощности на обработку 10К коннектов? Может хватить, может не хватить — зависит от «тяжести» вычислений и того, какая аппаратная часть. Или вопрос в том, могут ли 10К потоков работать независимор в рамках одной vm? Да, могут — в этом и смысл Эрланга вообще говоря.


  1. chaetal
    06.05.2019 14:18

    Автору респектище и земля пухом. Переводчику — спасибо. Статья отличная и архиполезная!
    …Но меня мучает ряд вопросов по поводу:

    К сожалению, хоть первый успешный объектно-ориентированный язык на основе этой модели (Smalltalk) и оперировал понятиями «объект» и «сообщение», но последние в Smalltalk были не настоящими сообщениями, а лишь замаскированными синхронными вызовами функций.
    Что не так с сообщениями в Smalltalk? Сообщения в ООП обязательно должны быть асинхронными и, если так, то почему? Или тут упор на «замаскированные вызовы функций»? Бывают другие варианты? В Erlang/Elixir это не так? А как?


    1. CheY
      06.05.2019 18:36
      +1

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


      1. chaetal
        07.05.2019 10:32

        То есть, претензия к языку заключается в том, что он не реализовал в себе все возможные варианты всего сущего? :) Честно говоря, сомневаюсь, что автор именно это имел ввиду. Аналогично тому как в Erlang/Elixir «gen_server позволяет нам эмулировать оба поведения», в Smalltalk можно реализовать поведение, которое складывает полученные сообщения в почтовый ящик и тут же возвращает управление отправителю.


        1. CheY
          07.05.2019 11:37

          И всё же такое поведение нельзя назвать синхронным. А если получатель упал не успев сообщить отправителю? В том-то и дело, что в Erlang асинхронность отправки сообщений между акторами является базовым вариантом, а уже синхронность эмулируется. Сообщения отправляются по принципу «выстрелил и забыл» и добавление любого ожидания ответа от получателя эту асинхронность ломает.


          1. chaetal
            07.05.2019 14:07

            А если получатель упал не успев сообщить отправителю?
            Отправитель получить исключительную ситуацию и может ее обработать — управление все равно вернется. Поведение, заданное как поведение «по умолчанию» в Erlang-е, в Smalltalk-е реализуется в рамках «библиотеки». Там есть куча всяких нюансов, не позволяющих сделать Smalltalk таким, каким он должен был бы быть, но, понимаете, подход такой: язык как можно проще (тут еще есть куда стремиться), все остальное — реализуется на этом языке.

            Кстати, некоторые называют это моделированием и обозначают как основную (объединяющую всех) идею ООП — еще одно определение? :)


            1. CheY
              07.05.2019 14:44
              +1

              Да, отправитель может обработать это исключение. Там же наверное и таймаут ожидания ответа будет. Но ведь именно об этом Джо Армстронг и пишет ниже, ратуя за простоту обмена сообщениями, простоту самих процессов-акторов и обработку ошибок вне процессов. Т.е. такие «инфраструктурные» сложности противоречат тезису Джо о том, что перечисленные выше атрибуты необходимы для распределённых и надёжных систем. В этом контексте он и указал на это, как на минус Smalltalk'а.
              Другое дело, что этот тезис можно поставить под сомнение. Но я не буду, так как скорее согласен с ним, да и Erlang/Elixir доказал, что на нём можно реализовывать такие системы малой кровью (сравнительно с другими).


              1. chaetal
                07.05.2019 14:58

                …необходимы для распределённых и надёжных систем. В этом контексте он и указал на это, как на минус Smalltalk'а
                Если бы речь была о том, что Erlang имеет преимущества перед Smalltalk в плане реализации распределенных систем — я бы не стал задавать дурацких вопросов. Но — перечитайте цитату еще раз — речь идет о том, что центральная для ООП и для Smalltalk концепция сообщений реализована в последнем как-то неполноценно. Аргументы, что конкаренси и параллелизм это круто, здесь не играют роли — разговор про ООП. Я вот и пытаюсь понять, в чем же претензии к реализации сообщений в Smalltalk — с точки зрения ООП, не распределенных систем и прочих посторонних вопросов совершенно другого уровня?


    1. Source
      06.05.2019 20:51

      Что не так с сообщениями в Smalltalk?

      В Smalltalk, строго говоря, вообще нет отдельной концепции сообщений. Т.е. говорить о сообщении как о чём-то независимом от объекта там можно только в теории.


      Бывают другие варианты? В Erlang/Elixir это не так? А как?

      В Erlang/Elixir каждый объект (актор) имеет свой mailbox, куда приходят сообщения, и он их по очереди обрабатывает. Ну и 2 режима отправки сообщений: ждать результат обработки сообщения от объекта или не ждать.


      1. irezvov
        06.05.2019 23:05

        Вообще говоря режим один — не ждать обработки сообщения, но gen_server позволяет нам эмулировать оба поведения


      1. chaetal
        07.05.2019 10:23

        В Smalltalk, строго говоря, вообще нет отдельной концепции сообщений.
        Как это?!? Если говорить о Smalltalk как о языке, то сообщения лежат в основе его синтаксиса — разве это не та самая «концепция»? Другими словами (если отбросить несколько исключений), Smalltalk — это и есть сообщения. Если речь о более «физическом» воплощении концепции сообщений, то ее можно даже «пощупать» — см. класс Message :)

        Т.е. говорить о сообщении как о чём-то независимом от объекта там можно только в теории.
        Не понимаю мысль. Если трактовать формально, то да — объекты и сообщения неразделимы, одно определяется через другое и наоборот: сообщение — это то, что мы посылаем объектам когда хотим попросить их что-то сделать (т.е. то, ради чего и существуют объекты); соответственно, объекты — это то, что умеет как-то реагировать на сообщения. Это видно и на уровне реализации: в Object есть методы, реализующие реакцию объектов на присланные сообщения (через примитивы виртуальной машины), и есть вышупомянутый класс Message, реализующий ту самую концепцию в среде Smalltalk.

        В этом есть что-то «плохое»? Или речь вообще о чем-то другом?

        В Erlang/Elixir каждый объект (актор) имеет свой mailbox, куда приходят сообщения, и он их по очереди обрабатывает. Ну и 2 режима отправки сообщений: ждать результат обработки сообщения от объекта или не ждать.
        Про это я в курсе, но чем это принципиально отличается от того, что есть в Smalltalk? Настолько принципиально, что в Smalltalk это сделано неправильно («не настоящими сообщениями, а лишь замаскированными синхронными вызовами функций»), а в Erlang — правильно? Я, кстати, так пока и не понимаю, что же такое сообщения в последнем: просто какая-то еще одна умозрительная (теоретическая?) концепция, не имеющая воплощение в самом языке, описанная где-то за его пределами и искусственно привнесенная в него. Нет?


    1. gBear
      07.05.2019 12:08

      Доброе…

      Что не так с сообщениями в Smalltalk?

      Если коротко, то «не так» там не с «сообщениями», но с тем, что называется словом messaging. «Сообщения» — как бы — есть, а вот «обмена сообщениями» — нет.

      Грубо говоря, «ответ» — это не «сообщение» :-(

      Сообщения в ООП обязательно должны быть асинхронными и, если так, то почему?

      «Сообщения» — сами по себе — не могут быть ни синхронными, ни асинхронными. Процесс обмена сообщениями — может. И асинхронный процесс выгодней, т.к. имея его, синхронный получается «на автомате».

      А вот из синхронного процесса обмена сообщениями, асинхронный — просто так — получить уже не получится.

      Бывают другие варианты? В Erlang/Elixir это не так? А как?

      В Erlang — «всё вообще не так» :-) Там нормальный messaging.


      1. chaetal
        07.05.2019 14:19

        Грубо говоря, «ответ» — это не «сообщение» :-(
        Вот это — почти верно. Можно было бы «отмазаться», выдвинув версию, что возврат значения — это часть сообщения (процесса обмена сообщениями), так, собственно и определяется: сообщение можно послать объекту, объект возвращает некоторый объект как объект… Но возврат значения «забит» в синтаксис и сообщением, действительно не является.

        На мой взгляд, это следствие заложенного в синтаксис «шаблона» посылки сообщения:
        получатель сообщение.
        Вот если бы сделать язык, где сообщение посылается «никому» (просто в окружающую среду — как в жизни), а кто хочет вылавливает и обрабатывает… но я об этом «мечтаю» уже несколько лет — никак не решусь облечь в слова :)

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

        А вот из синхронного процесса обмена сообщениями, асинхронный — просто так — получить уже не получится.
        И в чем проблема? Я в соседних комментах уже пару раз изложил свое видение проблемы.


        1. gBear
          07.05.2019 18:45
          +1

          Вот это — почти верно.

          Почти?!
          Можно было бы «отмазаться», выдвинув версию, что возврат значения — это часть сообщения (процесса обмена сообщениями),…

          Нельзя… return, емнип, по спекам не относится к message passing.
          На мой взгляд, это следствие заложенного в синтаксис «шаблона» посылки сообщения:

          Это не следствие синтаксиса… это модель вычисления такая. Синтаксис — сам по себе — уже «следствие».
          …Тем не менее, сообщения в Smalltalk-е есть, обмен сообщениями (с ответами на них) есть…

          Еще раз… сообщения есть, а *обмена сообщениями* — в смысле *messeging* — в Smalltalk — нет. То, что в Smalltalk есть — это эдакий «RPC — вид сбоку».
          … и почему их можно назвать «замаскированнными вызовами функций» для меня по-прежнему загадка. Если можете не коротко по этому поводу — было бы интересно таки услышать.

          :-) Это «замаскированные вызовы функций» по той простой причине, что никакой другой *семантики* у сообщений Smalltalk нет. Это *всегда* и *только* вызов метода.

          И это не синтаксис, опять же. Это модель вычисления у Smalltalk такая.

          А в Erlang — сообщение это «голые» данные, и только. Какую-либо семантику они (эти данные) обретают лишь в контексте конкретной ф-ции. Т.е. то, как данное конкретное сообщение будет (и будет ли) обработано тем процессом, которому оно послано, определяется лишь той функцией, которую данный процесс в этот момент вычисляет.
          И в чем проблема? Я в соседних комментах уже пару раз изложил свое видение проблемы.

          ?! «Проблема» в том, что в результате — в лучшем случае — получается плохой Erlang :-)


          1. chaetal
            07.05.2019 21:17

            то, как данное конкретное сообщение будет (и будет ли) обработано тем процессом, которому оно послано, определяется лишь той функцией, которую данный процесс в этот момент вычисляет
            То есть, сообщение в Smalltalk-е — это замаскированный вызов метода, а сообщение в Erlang-е — это (как раз) замаскированнный вызов функции? (…ну, или замаскированное выражение?)

            Нельзя… return, емнип, по спекам не относится к message passing.
            «In Smalltalk, a message will always return a value» — это часть реакции объекта на сообщение. Как и в Erlang-е, в Smalltalk внутри обработчика сообщений (к моему сожалению) не все делается через сообщения. И, так же как и в Erlang, мы же не будем из-за этого утверждать, что messaging отсуствует?

            Это не следствие синтаксиса…
            Если вы не поняли мысль, поясню. Smalltalk позволяет в Object реализовать метод типа #return:, который будет осуществлять возврат значения отправителю исходного сообщения. То есть, возврат можно сделать сообщением, например
            self return: 2 + 3.
            Тогда messaging в Smalltalk-е появится?

            Это не следствие синтаксиса… это модель вычисления такая.
            Какая такая? Уточните пожалуйста :)

            Еще раз… сообщения есть, а *обмена сообщениями* — в смысле *messeging* — в Smalltalk — нет. То, что в Smalltalk есть — это эдакий «RPC — вид сбоку».
            Повторение — оно конечно мать ученья, но только в том случае, если там есть содержательная часть. У вас она пока отсутствует — больше смахивает на агитацию или самовнушение. …А после слов про RPC у меня закрались сомнения: вы же в курсе, как сообщения работают в Smalltalk-е, да? В каком отношении это напомнило вам RPC?

            Это *всегда* и *только* вызов метода.
            Какого именно метода, можете ответить?

            Это «замаскированные вызовы функций» по той простой причине, что никакой другой *семантики* у сообщений Smalltalk нет. Это *всегда* и *только* вызов метода. … А в Erlang — сообщение это «голые» данные, и только. Какую-либо семантику они (эти данные) обретают лишь в контексте конкретной ф-ции.
            Сообщение в Smalltalk-е — это «голый» объект, семантику он получает только в контексте конкретного объекта. В чем разница?

            «Проблема» в том, что в результате — в лучшем случае — получается плохой Erlang :-)
            О, опять агитация? А может быть проблема в том, что должно хватать разума и опыта понимать, что не существует просто лучшего решения. Даже такого прекрасного, как Эрланг с акторами. Как у вас с этим?


            1. gBear
              07.05.2019 23:49
              +1

              То есть, сообщение в Smalltalk-е — это замаскированный вызов метода, а сообщение в Erlang-е — это (как раз) замаскированнный вызов функции? (…ну, или замаскированное выражение?)

              Послать сообщение в Erlang — означает ровно одно. Передать данные. И всё. Ничего «замаскированного» там нет… ни функций, ни выражений.

              Т.е. послать сообщение x процессу A — *буквально* — означает только то, что если процесс A в данный момент существует, в его mailbox будет добавлено сообщение x. И ничего более. *Если* (*когда*) процесс А будет вычислять функцию, в которой есть receive expression, и *если* под этот receive попадет сообщение x, то это — действительно — может привести к вычислению какой-то отдельной функции… а может и не привести.

              Но это вообще никак не зависит от *сообщения x*, а только лишь от той функции, в которой произойдет редукция такого receive.

              «In Smalltalk, a message will always return a value» — это часть реакции объекта на сообщение. Как и в Erlang-е, в Smalltalk внутри обработчика сообщений (к моему сожалению) не все делается через сообщения. И, так же как и в Erlang, мы же не будем из-за этого утверждать, что messaging отсуствует?

              Я, к сожалению, перестаю вас понимать. Причем тут вообще «реакция объекта на»?! Речь о том, что *результат* «реакции объекта» — т.н. return — это не message send, в терминах Smalltalk. Только и всего.

              Причем это *явно* отражено во всех спеках, которые я помню.

              Если вы не поняли мысль, поясню. Smalltalk позволяет в Object реализовать метод типа #return:, который будет осуществлять возврат значения отправителю исходного сообщения. То есть, возврат можно сделать сообщением, например self return: 2 + 3.
              Тогда messaging в Smalltalk-е появится?

              :-) Это вы, похоже, не понимаете о чем речь. Речь не о том, что *тип* return value не может быть «сообщением». Речь о том, что return это не message send expression. Вот и все.
              % Это send expression 
              A ! {self(), hello},
              ...
              % А это receive expression
              receive
                {Sender, Message} ->
                  %А вот это тот самый return, о котором речь... в Erlang - внезапно - это send expression.
                  Sender ! Message
              end,
              

              Вот об этом речь. О том, что «результат» *явно* отправляется. Механизм одинаковый, что в ту, что в обратную сторону. А не о том, есть return value или нет, часть это «реакции» или нет.

              Какая такая? Уточните пожалуйста :)

              Computational Model of Smalltalk Execution
              A message send causes execution of the currently active method to be temporarily suspended and for program execution to continue starting with the first expression of another method. A message send directs a message to an object. The object is called the receiver of the message. A message consists of a method selector and a set of arguments. Each argument is a reference to an object. When an object receives a message, the method selector of the message is used to select the method from the object's behavior that corresponds to the selector. The method becomes the new locus of execution. Special processing takes place if the receiver's behavior does not include a method corresponding to the message's method selector.

              A return terminates execution of a method and causes execution to resume within the method that executed the message send that activated the method containing the return. Execution continues immediately following the message send expression. The return provides a value (an object reference) that becomes the value of the message send.


              1. chaetal
                08.05.2019 09:59

                Я вот понять не могу… вы меня троллить чтоль пытаетесь? :-)
                Разумеется нет, я лишь пытался выяснить, на каком уровне идет дискуссия. И он меня вполне устраивает. Так что продолжаем? ;)

                Насчет Erlang
                Послать сообщение в Erlang — означает ровно одно. Передать данные. И всё. Ничего «замаскированного» там нет… ни функций, ни выражений.

                в его mailbox будет добавлено сообщение x

                будет вычислять функцию, в которой есть receive expression
                , и *если* под этот receive попадет сообщение x
                Не понимаю… вы ходите сказать, что поиск процесса по ID и помещение сообщения в его почтовый ящик, как и сопоставление сообщений из почтового ящика с образцом и выполнение соответствующих действий происходит неким магическим образом? Или же за этим скрывается что-то более банальное типа вызова функци или примитива ВМ… или, может быть, кода, вставленного компилятором?

                …А к обсуждению Smalltalk предлагаю вернуться после того, как договоримся о терминах на примере Erlang, если вы не против?


                1. AstarothAst
                  08.05.2019 11:11
                  +1

                  Или же за этим скрывается что-то более банальное типа вызова функци или примитива ВМ… или, может быть, кода, вставленного компилятором?

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


                1. gBear
                  08.05.2019 19:53

                  Не понимаю… вы ходите сказать, что поиск процесса по ID и помещение сообщения в его почтовый ящик, как и сопоставление сообщений из почтового ящика с образцом и выполнение соответствующих действий происходит неким магическим образом?

                  Нет. Я не хочу сказать, что это происходит «неким магическим образом». Я вообще не понимаю, к чему тут этот пассаж, что именно вы пытаетесь таким образом узнать?


  1. AstarothAst
    06.05.2019 15:11

    Или тут упор на «замаскированные вызовы функций»? Бывают другие варианты? В Erlang/Elixir это не так? А как?

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


    1. chaetal
      06.05.2019 16:10

      Чтение сообщения из почтового ящика — функция? Обработка очередного сообщения — функция? По-моему, как раз это можно назвать «замаскированными вызовами функций» :) или я ошибасюь? В Erlang-е или его ВМ вообще есть что-то кроме функций?

      Вот как раз Smalltalk-е функций нет вообще. Сообщения обрабатываются в соответствии определенной (нетривиальная) процедурой, которую очень сложно назвать «вызовом функции». Да, это процедура «прошита» в виртуалке (хотя, с ней можно «играться» в широком диапазоне, не трогая виртуалку), да передача сообщения — вещь синхронная (надо дождаться ответа). Хорошо это или плохо (или плохо что-то другое) — вопрос отдельный…

      Я просто хочу точно понять: данная фраза — просто не очень точная формулировка или за ней скрывается что-то действительно важное?


      1. AstarothAst
        06.05.2019 17:03

        Чтение сообщения из почтового ящика — функция?

        Нет, выражение.
        Обработка очередного сообщения — функция?

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

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

        Я просто хочу точно понять: данная фраза — просто не очень точная формулировка или за ней скрывается что-то действительно важное?

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


        1. chaetal
          06.05.2019 17:37

          Вы не поняли вопрос, спасибо за ответ.


          1. helions8 Автор
            06.05.2019 18:09

            Я бы точно так де ответил. В чём тогда был вопрос на самом деле?


            1. chaetal
              06.05.2019 21:52

              См. ниже


          1. AstarothAst
            06.05.2019 21:18

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


            1. chaetal
              06.05.2019 21:52

              См. ниже


      1. Source
        06.05.2019 21:00

        Я думаю, имелось в виду, что реализация обработки "сообщений" в Smalltalk ничем по сути не отличается от вызова метода в языках типа C++, Java и т.д. А в Erlang/Elixir она принципиально иначе реализована. Сообщение тут это не объект и не метод, а именно сообщение и ничто больше. Отправить его можно при желании кому угодно (любому актору), как и почтовый конверт любому адресату.


        1. chaetal
          06.05.2019 21:52

          Я думаю, имелось в виду, что реализация обработки «сообщений» в Smalltalk ничем по сути не отличается от вызова метода в языках типа C++, Java и т.д.
          Вот это «по сути» — можно раскрыть? Где коренится эта самая суть?

          Может быть она в том, что в Smalltalk-е для связывания сообщения с кодом используется имя сообщения (селектор), а в Erlang/Elixir паттерн-матчинг (если я правильно понимаю)? …Но так ли это принципиально?

          Сообщение тут это не объект и не метод, а именно сообщение и ничто больше.
          Разве в Erlang сообщение — не «просто» терм? …А в Smalltalk — «просто» объект (но никак не метод) ;)

          Отправить его можно при желании кому угодно (любому актору), как и почтовый конверт любому адресату.
          …так же, как и в Smalltalk-е…

          …Я так и не могу уловить эту самую вышеупомянутую суть.

          …И еще раз (прошу прощения за занудство): в цитате написано про вызов функций. Это еще можно отнести, например, к C++: действительно, косвенный вызов через таблицу виртуальных методов можно назвать «замаскированными вызовом функции», но как можно таким образом охарактеризовать Smalltalk-овский подход — все равно не понимаю :)


          1. helions8 Автор
            06.05.2019 22:30

            Может быть она в том, что в Smalltalk-е для связывания сообщения с кодом используется имя сообщения (селектор), а в Erlang/Elixir паттерн-матчинг (если я правильно понимаю)?


            Нет, это не так. Не уверен, что я смогу нормально объяснить, но всё же. В Erlang всё есть процессы (т.н. «грин-треды»), которые идентифицируются PID'ами. Каждый процесс исполняет какой-то произвольный код, с котором его запустили (я имею в виду, что никакого «типа» у процесса нет). Мейлбокс это свойство процесса, а не кода. Сообщения отправляются процессу (без деталей), зная его PID. Если код, выполняемый процессом не читает мейлбокс — значит, он его не читает. Всё. Можно долго отправлять такому процессу сообщения в одну сторону, пока мейлбокс не переполнится или процесс не закончится (выполнили весь код и вышли). Сообщения это просто данные. Можно читать, можно не читать. Можно читать и что-то делать, а можно читать и ничего не делать. Можно читать те, которые «нравятся», оставляя в очереди не подходящие. Отправка сообщений всегда односторонняя, у нее нет возвращаемого значения.

            Я не очень много знаю о Smalltalk, но из того, что я прочитал — это вообще не похоже на Erlang.


            1. chaetal
              07.05.2019 10:43

              Я не очень много знаю о Smalltalk, но из того, что я прочитал — это вообще не похоже на Erlang.
              Если заменить слова «процесс» и «PID процесса» на «объект» и ориентироваться не на асинхронную, а синхронную обработку сообщений (делающую почтовый ящик ненужным, но очевидным образом допускающую и реализацию описанного асинхронного варианта), то все становится гораздо более похожим ;)


              1. helions8 Автор
                07.05.2019 12:04
                +1

                Если заменить слова «процесс» и «PID процесса» на «объект»
                Нельзя заменить, у них разная семантика. Процесс — единица обработки/выполнения, объект — совокупность состояния и поведения. В Erlang объектом будет совокупность процесс + код, притом, при соблюдении некоторых дополнительных условий. Еще раз подчеркну — сообщения отправляются процессу, а не «объекту». Общего состояния у процессов нет.

                ориентироваться не на асинхронную, а синхронную обработку сообщений (делающую почтовый ящик ненужным ...)
                Опять нельзя. В рамках Erlang'овой модели у вас нет никаких гарантий, что что именно в этот момент, когда вы отправили сообщение, код, исполняемый процессом, будет этих самых сообщений ожидать. Собственно, нет гарантий, что процессом вообще будет исполнятся такой код, а это значит, что при синхронной модели (если б она была в Erlang) любая мелкая ошибка приведет к остановке всего — например, отправили сообщение процессу, код которого сообщения не читает -> ждем ответа бесконечно. Синхронная модель накладывает обязательства на получателя – это то, почему в Smalltalk есть протоколы. Еще раз повторюсь — в асинхронной модели Erlang на получателя не накладывается вообще никаких обязательств, именно потому, что модель асинхронная.

                Асинхронная модель эмулируется, это правда. Producer и consumer, связанные очередью – довольно известный шаблон. Синхронная «объектная» модель эмулируется в Erlang, но тоже посылкой сообщений, со всеми вытекающими последствиями. Замечу, что вызов обычных функции в Erlang — синхронный, если что, но это не про акторы и процессы. Любая эмуляция остается эмуляцией, из под которой протечет настоящая модель.

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


                1. chaetal
                  07.05.2019 14:44

                  Процесс — единица обработки/выполнения, объект — совокупность состояния и поведения. … Общего состояния у процессов нет
                  Вы меня пугаете! :D Когда речь идет о том, что Erlang/Elixir поддерживают объектную парадигму лучше, чем некоторые «объектно-ориентированные» языки (и я с этим согласен), то проводите явную параллель между объектами и процессами (или акторами? или это одно и то же?) — и вдруг на попятную!?! :) У процессов разве нет локального состояния? В чем же такая принципиальная разница? Ориентация на concurrency и хорошая поддержка параллелизма — это фичи, а не принципиальная разница.

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

                  …Вторую часть вашего сообщения я, пожалуй, не буду комментировать, по крайней мере до тех пор, пока мы не начнем говорить об одном и том же :)


                  1. helions8 Автор
                    07.05.2019 15:24

                    Я нигде не говорил ни про лучше, ни про хуже. Я вам пытаюсь объяснить то, как оно работает в Erlang, и какие сущности могут быть сопоставлены с понятием «объект» в других языках, в данном случае Smalltalk, и как работает механизм взаимодействия «объектов». А вы пытаетесь просто один к одному переложить понятия из одной экосистемы на другую – это не работает. Процесс это не объект, код это не объект и не класс. Стейт мейлбокса процесса не связан со стейтом кода, выполняемого процессом.

                    Акторная модель — это математическая модель прежде всего, для которой Erlang стал очень удобной платформой, позволяя ее реализовать посредством встроенных в платформу и язык фич, одна из которых — процессы с мейлбоксами. Но если вы создали процесс, и в качестве кода ему передали функцию, которая делает ничего — это не актор. Актор это то, что 1) принимает сообщения 2) посылает сообщения 3) создает другие акторы. Это концепция, в конце-то концов.

                    Ориентация на concurrency и хорошая поддержка параллелизма — это фичи, а не принципиальная разница.


                    Это очень, очень спорное заявление. Весь пример Erlang'а показывает, как введение определенных примитивов параллелизма в язык очень сильно меняет подход разработки.

                    Еще раз, не пытайтесь «смапить» сущности из одной парадигмы на другую 1 к 1, это очень часто не работает.


                    1. chaetal
                      07.05.2019 15:40

                      Я вам пытаюсь объяснить то, как оно работает в Erlang
                      Спасибо, но на том уровне, на каком вы мне объясняете — я в курсе :)
                      А вы пытаетесь просто один к одному переложить понятия из одной экосистемы на другую – это не работает.
                      Вы, похоже, неверно поняли, что хочу… В общем, еще раз спасибо за пояснения, освежить знания и услышать новые аспекты всегда полезно.


            1. chaetal
              07.05.2019 10:48

              Вот чем Erlang-овская ООП действительно принципиально отличается от Smalltalk-овской, так это тем, что объектность в первом заканчивается на уровне… не знаю как назвать, но, грубо говоря, заканчивается гораздо раньше чем в Smalltalk-е :)