Статьи

Функциональное программирование: сохранение безопасности типов

обзор

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

Цель FP — абстрагировать операции с данными с функциями, чтобы максимизировать возможность многократного использования и уменьшить изменения состояния и побочные эффекты в вашем приложении. Случается, что благодаря этому ваш код становится гораздо более масштабируемым, читаемым и тестируемым.

Функциональное программирование требует, чтобы функции имели 2 важных качества:

  1. Быть первоклассными гражданами
  2. Быть высокого порядка

Функции являются первоклассными, когда их можно создавать и назначать переменной. Функции высшего порядка, когда их можно использовать в сочетании с другими функциями, независимо от того, передается ли это в качестве аргумента или возвращается из функции. Давайте расширим это понятие дальше. Когда функция эквивалентна значению, она следует принципу
замещаемости , что означает, что возвращаемое значение вызова функции может быть заменено и встроено в выражение без изменения его результата. Рассмотрим быстрый пример JavaScript:

   function areaSquare(a) {
        return a * a;
   }

   function volumeCube(areaOfSquare, a) {
        return areaOfSquare * a;
   }

   a = 2;
   area = areaSquare(a);         // -> 4
   volume = volumeCube(area, a); // -> 8

Следующие выражения эквивалентны:

   volume = a * a * a; 

   volume = areaSquare(a) * a; 

   volume = volumeCube(areaSquare(a)); 

Почему JavaScript?

Проще говоря, вездесущность. JavaScript везде На протяжении многих лет язык Интернета, а теперь он также выходит на рынок серверов. Самое главное, JavaScript — это функциональный язык; фактически он поддерживает запись как в функциональных, так и в объектно-ориентированных стилях Это, я утверждаю, должно быть единственным способом, которым мы должны использовать JavaScript.

Кроме того, JavaScript не является типизированным языком. Хотя типы принудительно применяются, это делается во время выполнения, а не во время компиляции. На самом деле вы не включаете информацию о типе в ваш код. Учитывая, что инвариант, что ссылочно-прозрачные функции должны быть предсказуемыми и возвращать одно и то же значение на одном и том же входном сигнале при вызове, естественно, что функции должны быть согласованными по типу значения или объекта, которые они возвращают. Относительно прозрачные функции не имеют побочных эффектов и зависят только от предоставленного ввода.

Функции как:

Date.now();

и

   var counter = 0;                    // global
   function incrementCounterTo(num) {
 return counter += num;    
   }

не считаются чистыми из-за побочных эффектов, которые нарушают ссылочную прозрачность.

Сохранение безопасности типа

Чтобы поговорить о проблеме безопасности типов, рассмотрим следующий код:

   var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
                 "Aug", "Sep", "Oct", "Nov", "Dec"];
   function getMonthName(mo) {
      if (months[mo - 1] !== undefined) {
         return months[mo - 1];
      } 
      else {
         throw new Error("Invalid Month Error!");
      }
   }

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

    try {
        var m = getMonthName(13);

        // do work with m
    }
    catch (e) {
        return e.message;
    }  

Кроме того, эта функция нарушает ссылочную прозрачность, поскольку вы ни при каких обстоятельствах не можете встроить оператор исключения как часть большего выражения, так как он изменит весь смысл выражения.

Давайте посмотрим на более функциональный подход, который решит все эти проблемы.

Необязательный

Решение основано на основной концепции функционального программирования, называемой монадой . В частности, мы будем иметь дело с опциональной монадой, также называемой монадой Maybe в некоторых функциональных языках программирования. Вот код для Необязательного объекта JavaScript (некоторые детали опущены для краткости):

var Optional = (function () {

    // private constructor
    function Option(val) {
        var _value = val || null;

        // public methods

        this.get = function () {
            if (!this.isPresent()) {
                throw 'NoSuchElementError';
            }
            return _value;
        };
        
        this.map = function (fn) {
            if (!this.isPresent()) {
                return Optional.empty();
            }
            if (typeof fn == 'function') {
                return Optional.ofNullable(fn.call(_value));
            }
            return Optional.ofNullable(_value[fn].call(_value));
        };

        this.getOrElse = function (other) {
            return _value !== null ? _value : other;
        };
    }

    return {

        empty: function () {
            return Object.freeze(new Option());
        },
        
        of: function (val) {
            if (val === null) {
                throw 'NoSuchElementError';
            }
            return Object.freeze(new Option(val));
        },
        
        ofNullable: function (val) {
            var inst = val !== null ? this.of(val) : this.empty();
            return Object.freeze(inst);
        }
    }
})();

Есть много способов сделать это, мне нравится понятие неизменности в объектах, поэтому я предпочитаю замораживать объекты по мере их создания. Используя Optional, мы сохраняем ссылочную прозрачность, а также сохраняем безопасность типов в наших функциях. Это имеет много преимуществ:

  1. Создайте четкий контракт для наших функций
  2. Предотвратите наших звонящих от необходимости помещать шаблонный код обработки исключений в каждый вызов
  3. Обеспечить более краткий и свободный опыт API

Давайте посмотрим, как мы можем улучшить нашу примерную задачу:

   function findMonthName(months, mo) {
        if (months[mo - 1] !== undefined) {
            return Optional.of(months[mo - 1]);
        }
        else {
            return Optional.empty();
        }
    }

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

findMonthName(months, 13).getOrElse('No month found');

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

Вот еще один вариант использования этого функционального шаблона. Рассмотрим следующие простые объекты:

    function Address(country) {
        var _country = country;

        this.getCountry = function() {
            return _country;
        }
    }

    function Person(addr) {
        var _address = addr;

        this.getAddress = function() {
            return _address;
        }
    }

    function Customer(person) {
        var _person = person;

        this.getPerson = function() {
            return _person;
        }
    }

Было бы неплохо иметь возможность получить доступ к адресу клиента как:

customer.getPerson().getAddress().getCountry()

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

    var p = customer.getPerson();
    if(p !== null) {
        var a = p.getAddress();
        if(a !== null) {
            var c = a.getCountry();
            return c;
        }
        return null;
    }
    return null;

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

Давайте теперь посмотрим на более масштабируемый и идиоматический подход, функциональный способ:

   customer.getPerson()
            .map('getAddress')
            .map('getCountry')
            .getOrElse('Unknown');    

Если в цепочке вызовов отсутствует какое-либо значение, это приведет к короткому замыканию и возврату «Неизвестно». Мы делаем это, чтобы избежать всех нулевых проверок. Все, что нам нужно сделать, это настроить наш API на основе Optional :

   function Customer(person) {
        var _person = person;

        this.getPerson = function() {
            return Optional.ofNullable(_person);
        }
    }

Вот и все!

Функциональное программирование — очень важный и мощный способ программирования. Эксперты в данной области осознали, что приложения, написанные в функциональном стиле, имеют тенденцию быть более выразительными, удобочитаемыми и правильными. Ссылочные прозрачные функции могут иметь свои результирующие значения, подставляемые вместо встроенного вызова функции. Наконец, использование Optional может улучшить ваш код разными способами, сделав его более коротким и легким для написания.