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

Предыдущие части: Часть 1, Часть 2, Часть 3, Часть 4

Ссылочная прозрачность

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

В алгебре, когда у вас была следующая формула:

y = x + 10

И сказали:

x = 3

Вы можете снова подставить x в уравнение, чтобы получить:

y = 3 + 10

Обратите внимание, что уравнение все еще в силе. Мы можем сделать такую ​​же замену с чистыми функциями.

Вот функция в Elm, которая помещает указанную строку в одинарные кавычки:

quote str =
    "'" ++ str ++ "'"

А вот код, который его использует:

findError key =
    "Unable to find " ++ (quote key)

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

Поскольку функция quote является чистой, мы можем просто заменить вызов функции в findError телом quote (которая является просто выражением):

findError key =
   "Unable to find " ++ ("'" ++ str ++ "'")

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

Это может быть особенно полезно при рассмотрении рекурсивных функций.

Порядок исполнения

Большинство программ являются однопоточными, то есть одновременно выполняется один и только один фрагмент кода. Даже если у вас многопоточная программа, большинство потоков блокируются в ожидании завершения ввода-вывода, например файл, сеть и т. д.

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

1. Get out the bread
2. Put 2 slices into the toaster
3. Select darkness
4. Push down the lever
5. Wait for toast to pop up
6. Remove toast
7. Get out the butter
8. Get a butter knife
9. Butter toast

В этом примере есть две независимые операции: получение масла и поджаривание хлеба. Они становятся взаимозависимыми только на шаге 9.

Мы могли бы выполнять шаги 7 и 8 одновременно с шагами с 1 по 6, поскольку они не зависят друг от друга.

Но как только мы это сделаем, все усложняется:

Thread 1
--------
1. Get out the bread
2. Put 2 slices into the toaster
3. Select darkness
4. Push down the lever
5. Wait for toast to pop up
6. Remove toast
Thread 2
--------
1. Get out the butter
2. Get a butter knife
3. Wait for Thread 1 to complete
4. Butter toast

Что произойдет с потоком 2, если поток 1 выйдет из строя? Каков механизм согласования обоих потоков? Кому принадлежит тост: Тема 1, Тема 2 или обе?

Проще не думать об этих сложностях и оставить нашу программу однопоточной.

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

Однако есть 2 основные проблемы с многопоточностью. Во-первых, многопоточные программы сложно писать, читать, анализировать, тестировать и отлаживать.

Во-вторых, некоторые языки, например Javascript не поддерживает многопоточность, а те, которые поддерживают, плохо поддерживают.

Но что, если порядок не имеет значения и все выполняется параллельно?

Это звучит безумно, но не так хаотично, как кажется. Давайте посмотрим на код Elm, чтобы проиллюстрировать это:

buildMessage message value =
    let
        upperMessage =
            String.toUpper message
        quotedValue =
            "'" ++ value ++ "'"
    in
        upperMessage ++ ": " ++ quotedValue

Здесь buildMessage принимает message, а value затем создает сообщение в верхнем регистре и двоеточие и значение в одинарных кавычках.

Обратите внимание на независимость upperMessage и quotedValue. Откуда нам это знать?

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

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

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

В этом случае оба upperMessage и quotedValue являются чистыми и ни один из них не требует вывода другого.

Следовательно, эти 2 функции могут быть выполнены в ЛЮБОМ ПОРЯДКЕ.

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

Порядок выполнения на чистом функциональном языке может быть определен компилятором.

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

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

Благодаря Pure Functional Languages ​​у нас есть возможность автоматически использовать преимущества ядер ЦП на мелкомасштабном уровне, не изменяя ни единой строчки кода.

Аннотации типов

В языках со статической типизацией типы определены встроенными. Вот пример кода Java для иллюстрации:

public static String quote(String str) {
    return "'" + str + "'";
}

Обратите внимание, как типизация соответствует определению функции. Еще хуже, когда у вас есть дженерики:

private final Map<Integer, String> getPerson(Map<String, String> people, Integer personId) {
   // ...
}

Я выделил типы жирным шрифтом, которые выделяют их, но они по-прежнему мешают определению функции. Вы должны внимательно прочитать его, чтобы найти имена переменных.

С динамически типизированными языками это не проблема. В Javascript мы можем написать такой код:

var getPerson = function(people, personId) {
    // ...
};

Это намного легче читать, не мешая всей этой неприятной информации о типах. Единственная проблема в том, что мы отказываемся от безопасности набора текста. Мы могли бы легко передать эти параметры в обратном порядке, то есть Number для людей и Object для personId.

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

Но что, если бы у нас было лучшее из обоих миров. Синтаксическая простота Javascript с безопасностью Java.

Оказывается, можем. Вот функция в Elm с аннотациями типов:

add : Int -> Int -> Int
add x y =
    x + y

Обратите внимание, как информация о типе находится в отдельной строке. Это разделение имеет огромное значение.

Теперь вы можете подумать, что в аннотации типа есть опечатка. Я знал, что сделал это, когда впервые увидел это. Я думал, что первая - › должна быть запятой. Но опечатки нет.

Когда вы видите это с подразумеваемыми круглыми скобками, это имеет немного больше смысла:

add : Int -> (Int -> Int)

Это говорит о том, что add - это функция, которая принимает единственный параметр типа Int. и возвращает функцию, которая принимает единственный параметр Int и возвращает Int.

Вот аннотация другого типа с подразумеваемыми круглыми скобками:

doSomething : String -> (Int -> (String -> String))
doSomething prefix value suffix =
    prefix ++ (toString value) ++ suffix

Это говорит о том, что doSomething - это функция, которая принимает единственный параметр типа String. и возвращает функцию, которая принимает единственный параметр типа Int и возвращает функцию, которая принимает единственный параметр типа String и возвращает String.

Обратите внимание, как все принимает единственный параметр. Это потому, что каждая функция каррирована в Elm.

Поскольку скобки всегда подразумеваются справа, в них нет необходимости. Итак, мы можем просто написать:

doSomething : String -> Int -> String -> String

Скобки необходимы, когда мы передаем функции в качестве параметров. Без них аннотация типа была бы неоднозначной. Например:

takes2Params : Int -> Int -> String
takes2Params num1 num2 =
    -- do something

сильно отличается от:

takes1Param : (Int -> Int) -> String
takes1Param f =
    -- do something

take2Param - это функция, для которой требуются 2 параметра: Int и еще один Int . Принимая во внимание, что для take1Param требуется 1 параметр, функция, которая принимает Int и еще один Int .

Вот аннотация типа для карты:

map : (a -> b) -> List a -> List b
map f list =
    // ...

Здесь скобки необходимы, потому что f имеет тип (a - ›b), то есть функция, которая принимает единственный параметр типа a и возвращает что-то типа b.

Здесь тип a - это любой тип. Когда тип написан в верхнем регистре, это явный тип, например Строка. Когда тип написан в нижнем регистре, он может быть любым. Здесь a может быть String, но также может быть Int .

Если вы видите (a - ›a), значит, тип ввода и тип вывода ДОЛЖНЫ быть одинаковыми. Неважно, какие они, но они должны совпадать.

Но в случае map у нас есть (a - ›b). Это означает, что он МОЖЕТ возвращать другой тип, но МОЖЕТ также возвращать тот же тип.

Но как только тип для a определен, a должен быть этим типом для всей подписи. Например, если a равно Int и b равно String, тогда подпись эквивалентна:

(Int -> String) -> List Int -> List String

Здесь все a были заменены на Int и все b заменены на String.

Тип List Int означает, что список содержит Int и List String означает, что список содержит String. Если вы использовали дженерики на Java или других языках, эта концепция должна быть вам знакома.

Мой мозг!!!!

А пока хватит.

В заключительной части этой статьи я расскажу о том, как вы можете использовать полученные знания в своей повседневной работе, то есть функциональный Javascript и Elm.

Наверх Далее: Часть 6

Если вам это понравилось, нажмите ниже, чтобы другие люди увидели это здесь, на Medium.

Обновление около 2021 года: у меня есть книга, в которой вы узнаете все из этой серии и многое другое: Упрощенное функциональное программирование: пошаговое руководство.

Если вы хотите присоединиться к сообществу веб-разработчиков, которые учатся и помогают друг другу в разработке веб-приложений с использованием функционального программирования в Elm, посетите мою группу в Facebook, Learn Elm Programming https : //www.facebook.com/groups/learnelm/

Мой Twitter: @cscalfani