Пятничный JS: как вдохновиться Smalltalk'ом и попасть в ад

    Когда я читал книгу «Паттерны разработки игр», написанную замечательным человеком по имени Bob Nystrom (я не пишу его имя по-русски, поскольку не имею ни малейшего понятия, как это произносится), в одной из глав мне на глаза попалась небольшая ода языку Smalltalk как праотцу всех современных объектно-ориентированных языков, намного опередившему своё время. Поскольку я по жизни испытываю необоримую приязнь ко всяким винтажным языкам, естественно, я полез про него гуглить. И разумеется, вместо того, чтобы вынести из этого опыта что-то полезное, я научился плохому.



    Особенность языка Smalltalk, за которую зацепился мой взгляд — это отсутствие специально обученных управляющих конструкций. Вместо них control flow реализуется с помощью отправки сообщений объектам. Например, если отправить объекту типа Boolean сообщение ifTrue с блоком кода в качестве дополнительного аргумента, этот код будет исполнен тогда и только тогда, когда значение булевского объекта будет истинным.

    result := a > b
        ifTrue:[ 'greater' ]
        ifFalse:[ 'less or equal' ]

    Словосочетание «булевский объект» звучит несколько странно, если не знать, что в Smalltalk нет простых значений: каждое значение является объектом. «Постойте-ка! — воскликнул я, — что-то мне это напоминает!» И всё заверте…

    Дисклеймер
    Если вы сделаете что-то похожее в продакшн-коде, вы попадёте в ад. И там никто не станет с вами дружить. Даже Гитлер. У Гитлера, по крайней мере, была какая-то цель.

    В JavaScript не всякое значение является объектом. Однако там есть ещё более забавная вещь: автоматическое приведение типов. Каждый раз, когда мы пытаемся использовать простое значение как объект (скажем, получить доступ к его свойству), оно «оборачивается» в соответствующую объектную обёртку. Именно благодаря этому мы можем написать что-нибудь вроде true.toString(). Значение true не имеет метода toString, его имеет объект new Boolean(true). Если задуматься, это иронично: даже когда мы пытаемся сделать явное приведение типов, мы неявно (простите за тавтологию) используем неявное.

    К этой объектной обёртке, точнее, к её прототипу, мы можем «прицепить» свои собственные методы. Это не очень хорошая идея: если все станут так делать, рано или поздно возникнет коллизия. Какая-нибудь маленькая библиотека для работы с буфером обмена переопределит метод String.prototype.foo, который до этого определил какой-нибудь виджет для валидации пользовательского ввода. Могу вас уверить, виджету это не понравится. Но поскольку сегодня пятница, и мы не собираемся (не собираемся ведь?) использовать это в коде, с которым потом будут работать невинные люди, можно позволить себе немного Тёмных Искусств.

    Начнём с некоего аналога смолтоковского ifTrue.

    Boolean.prototype.ifThenElse = function(trueCallback, falseCallback){
        return this.valueOf() ? trueCallback() : falseCallback();
    }
    

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

    (2 * 2 == 5).ifThenElse(
        //надеюсь, в 2017 году стрелочные функции уже никого не смущают
        () => alert("Freedom is Slavery"),
        () => alert("O brave new world!")
    )
    

    Есть несколько нюансов. Во-первых, интерпретатор будет ругаться на нас ошибками, если мы передадим в качестве аргумента не функцию, а что-то другое (например, ничего). Во-вторых, в JS существует традиция (пришедшая ещё из C, где не было булевского типа) использовать в конструкции if не только логические значения, а вообще какие попало. В нормальном случае автоматическое приведение типов сделает всю грязную работу, превратив «falsy» значения в false, а остальные в true, но в нашем случае этого не произойдёт:

    (2 * 2).ifThenElse(
        () => alert("Freedom is Slavery"),
        () => alert("O brave new world!")
    ) // расскажет нам поучительную историю о том, что undefined is not a function
    

    Значит, нужно лезть выше. Вместо того, чтобы добавлять метод в прототип Boolean, добавим его в прототип Object. Звучит как отличный план, не так ли?

    function call(arg){
        return typeof arg == "function" ? arg() : arg;
    }
    Object.prototype.ifThenElse = function(trueCallback, falseCallback){
        if(this.valueOf()){
             return call(trueCallback);
        }else{
             return call(falseCallback);
        }
    }
    

    Почти хорошо. Теперь наш метод есть у чисел, строк, объектов… но не у undefined и не у null. К счастью или к сожалению, у них объектная обёртка отсутствует. К сожалению, это очень распространённые ложные значения. Впрочем, эту проблему легко решить:

    const nil = {
        valueOf: () => false
    }
    //всегда используйте nil вместо null
    //уже слишком толсто, да?
    

    Давайте определим ещё пару полезных методов.

    Number.prototype.for = function(callback){
        for(let i = 0; i < this.valueOf(); i++){
            callback(i);
        }
    }
    
    function countdown(n){
        console.log(10 - n);
    }
    
    10..for(countdown); //две точки нужны, поскольку десятку с одной точкой js воспринимает как литерал числа с плавающей точкой

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

    Object.prototype.forIn = function(callback){
        for(let key in this){
            callback(key, this[key], this);
        }
    }
    
    Object.prototype.forOwn = function(callback){
        for(let key in this){
            if(this.hasOwnProperty(key)){
                callback(key, this[key], this);
            }
        }
    }
    
    var obj = {foo: "bar"};
    obj.forIn(key => console.log(key)); // "forIn", "forOf", "foo" 
    //а также "ifThenElse", если вы исполняли предыдущий код в том же контексте
    obj.forOwn(key => console.log(key)); // "foo"

    Это уже даже похоже на нечто полезное. Не дайте этой похожести себя обмануть.

    Function.prototype.while = function(callback){
        while(this()){
            callback();
        }
    }
    
    var power = 5;
    var result = 2;
    
    (() => --power).while(
        () => result *= 2
    )
    

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

    И наконец:

    String.prototype.switch = function(callbackObject){
        var f = callbackObject[this.valueOf()];
        return typeof f == "function" ? f() : f;
    };
    
    ("1" + 2).switch({
        "12": () => console.log("Это JS"),
        "3": () => console.log("Это не JS")
    })
    

    По сравнению со стандартным JS'овским switch у этого метода есть несколько недостатков. Во-первых, он работает только для строк. При желании можно расширить его на произвольные значения, используя вместо объекта Map, но моё желание делать аморальные вещи на сегодня иссякло, и я предоставляю это любознательному читателю. Во-вторых, нет default. В-третьих, отсутствует возможность делать такие штуки:

    switch(value){
        case 1:
        case 2:
            console.log("Это единица или двойка");
            break;
        case 3:
            console.log("Точно тройка");
        case 4:
            console.log("Тройка или четвёрка");
    }
    

    Впрочем, многие скажут, что это скорее достоинство.

    Что ж, надеюсь, вам было так же весело, как и мне. А мне пора возвращаться к работе — ну, к настоящей работе. Где так не пишут. Ну, вы меня поняли.
    Share post

    Comments 39

    • UFO just landed and posted this here
        +11
        Если в коде, производительность которого хоть кому-то интересна, происходит такое, оверхед — меньшая из проблем.
          0

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

            0
            Если оборачивать, то да, на первый план выходят тысячи оверхеда. Или не тысячи. Тут уж смотря как обернуть.
              0

              Тогда выходит, что тема статьи — не надо шутить с методами встроенных объектов. И не смоллтолком вымощена дорога в ад, и даже не конструкцией из смоллтолка, а желанием реализовать её точь в точь, как там.


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

                +1
                Я полностью согласен с тем, что вы написали, и нигде не утверждал обратного)
                  –1

                  Да, я заметил, но у вас название статьи плохо бьётся с её содержимым.

        +4

        Ваши вкусі мне определнно нравятся))

          +1
          Я что-то посмотрел и так вышло, что это единственный коммент, на который я не ответил. Нехорошо получилось. Так что спасибо =)
          +4
          Вы так классно пишете, прочитал с огромным удовольствием :)
          Спасибо!
            +1
            На здоровье)
            +3
            Жаль в js нет перезагрузки операторов, вот так бы началось веселье!!!
              +2
              Неистово плюсую ваши слова. К счастью или к сожалению, W3C не спешит исправлять этот фатальный недостаток. Впрочем, пока можно использовать это.
                +1
                Она понемногу появляется, можно например перезагружать оператор for-of через переопределения Symbol.iterator, можно перезагружать оператор точка (получение свойства) через прокси и т.д. Все к этому движется в основном благодаря символам, арифметические операции выполняются по другим правилам (если встречается объект он приводится с помощью Symbol.toPrimitive к примитиву) но возможно они введут новый символ который будет создавать контекст для операции (что-то по типу того как делается в фейковой перегрузке операторов, только не глобальный контекст, а именно локальный для операции).
                  +1
                  Прикольно было бы как в Lua. Если у операнда есть метод, соответствующий перекрываемому оператору, то вместо стандартной операции вызывается он. В Lua не совсем так, но для JS — самое то. По символу на каждый оператор.
                    +1
                    Да, я тоже мечтаю именно о такой реализации, но пока основная загвоздка в механизме работы арифметических операций, пока операнды приводятся к примитивам независимо друг от друга ничего об операции не зная и до выполнения операции, это хорошо работает для примитивов, но плохо сочетается с перезагрузкой, поэтому нужна новая схема применения операторов. Мне кажется через контекст операции это реализуется проще всего. Сначала каждый из операндов регистрирует себя в контексте операции (сейчас с помощью Symbol.toPrimitive можно регистрироваться в глобальном контексте), а затем во время вычисления операции нужно проверять — если контекст не пустой, то уже зная операцию вызывать нужный переопределенный метод для операндов из контекста (сейчас этого шага нет и операция просто проходит с примитивами, а в случае с фейковой перезагрузкой нужно иметь какой-то оберточный метод который будет проводить данную операцию для возвращения результата из контекста). Это кажется наименее затратной схемой которая минимально меняет правила применения операндов и видимо не особо повлияет на производительность для операций с примитивами (добавится простая проверка пустоты контекста).
                      0
                      Не понял, что за зверь «контекст операции». Допустим, на вход операции поступило два операнда. Базовая функция операции проверяет наличие нужного метода у первого операнда, если нет — у второго, если нет у обоих — приводит оба к примитивам и ищет правила уже для примитивов.
                        0
                        Сейчас контекста операции нет и работает это не так, просто независимо друг от друга операнды без знания операции приводятся к примитивам, а затем происходит операция. Если проверять как говорите вы — прийдется сильно менять существующую схему работы. Как оно будет реализовано в итоге и будет ли пока загадка, я просто предположил как это может быть сделано дешево, с минимальными изменениями и добавлением дополнительного контекста.
                +3
                А почему вы times назвали for? Ну типа я не рубист, но 10..times(countdown) более человекочитаемо чем 10..for(countdown).
                  0
                  Ну, тематика псто — замена стандартных управляющих конструкций своими велосипедами. Конструкции times в js нет, потому назвал for. Абсолютно согласен, что ваш вариант читаемее.
                  +2

                  Есть ведь уже Amber:


                  a > b
                    ifTrue: [ 'greater' ]
                    ifFalse: [ 'less or equal' ].

                  Наслаждайтесь ;-)

                    0
                    Какая прелесть ^^
                    +2
                    Только не бросай не надо смотреть в сторону Forth.
                    Там тоже плохому научат…

                    Жду с нетерпением
                      0

                      Сейчас чего только нет поверх JS, уверен, что и Forth найдётся...

                        0
                        Сходу нашлось: jsforth
                          0
                          Совершенно неправильный «Форт». Вместо определения словаря для слов, там большущая цепочка вида else if (token == "drop")
                      +4
                      Да, улыбнуло классно! Главное, чтоб действительно никто такое не стал всерьез использовать :) А то ведь даже enum BOOLEAN {TRUE, FALSE, FILE_NOT_FOUND} способен найти своих поклонников…
                        –1
                        Аплодирую стоя :)
                          +2
                          Будто в цирк сходил. Спасибо
                          • UFO just landed and posted this here
                              0
                              Однако там есть ещё более забавная вещь: автоматическое приведение типов. Каждый раз, когда мы пытаемся использовать простое значение как объект (скажем, получить доступ к его свойству), оно «оборачивается» в соответствующую объектную обёртку.
                              Это не приведение типов, это боксинг/анбоксинг, приведение типов это когда мы хотим объек со строкой или строку с числом сложить.

                              (2 * 2 == 5).ifThenElse(
                                  //надеюсь, в 2017 году стрелочные функции уже никого не смущают
                                  () => alert("Freedom is Slavery"),
                                  () => alert("O brave new world!")
                              )
                              
                              А зачем это так реализовывать если есть достаточно много красивых функциональных библиотек для js? Разница будет только в том, что вы монаду объявите явно
                              If(2 * 2 === 5).fold(
                                  () => alert("O brave new world!"),
                                  () => alert("Freedom is Slavery")
                              );
                              
                                0
                                Это не приведение типов, это боксинг/анбоксинг, приведение типов это когда мы хотим объек со строкой или строку с числом сложить.
                                У Флэнагана это называлось приведением типов. Судя по результатам беглого гугления, правы скорее вы, чем он. Но, перефразируя Ньютона, «если я соврамши, то только потому, что стоял на плечах гигантов».

                                А зачем это так реализовывать
                                Потому что могу)
                                  0
                                  Потому что могу)

                                  Так наверняка можете и что-то более производительное и рабочее сделать тут главное не превращать все в монады, а оборачивать. Если решите поиграться — смотрите на спецификацию FantasyLand.
                                    0
                                    Пока что я придерживаюсь мнения, что экосистеме JS будет лучше без ещё одной либы)
                                      +1
                                      Просто никому ее не показывайте и вред будет минимальным =)
                                +1
                                «Ага!!!» — укоризненно сказали суровые сибирские лесорубы и ушли рубить лес топорами…
                                  0
                                  Sirion, а вы не перепутали профессию? Литературный талант заметен невооруженным глазом.
                                    0
                                    Ну, цитируя Олега нашего Дивова, «писатель – это прежде всего крепкая задница, железная сила воли и умение концентрироваться». А я натура увлекающаяся и отвлекающаяся.
                                    0
                                    офигенно :)

                                    Only users with full accounts can post comments. Log in, please.