Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
switch guess{
case 7: return "Much 7 very wow."; break;
}Или так, через подобие тернарного оператора:(guess == 7)?return ("Much 7 very wow.")Только более компактно.Основные императивные языки наших дней (такие как C, C++, Java) развивались в век классической многопоточности – в старые добрые времена простых архитектур памяти, понятных примитивов взаимоблокировки и разделения данных без изысков. Языки, естественно, моделировали реалии аппаратного обеспечения того времени (когда подразумевалось, что потоки разделяют одну и ту же область памяти) и включали соответствующие средства. В конце концов само определение многопоточности подразумевает, что все потоки, в отличие от процессов операционной системы, разделяют одно общее адресное пространство.
Кроме того, API для реализации обмена сообщениями (например, спецификация MPI) были доступны лишь в форме библиотек, изначально созданных для специализированного дорогостоящего аппаратного обеспечения, такого как кластеры (супер)компьютеров.
Тогда еще только зарождающиеся функциональные языки заняли принципиальную позицию, основанную на математической чистоте: «Мы не заинтересованы в моделировании аппаратного обеспечения, – сказали они. – Нам хотелось бы моделировать математику». А в математике редко что-то меняется, математические результаты инвариантны во времени, что делает математические вычисления идеальным кандидатом для распараллеливания. (Только представьте, как первые программисты – вчерашние математики, услышав о параллельных вычислениях, чешут затылки, восклицая: «Минуточку!..») Функциональные программисты убеждены, что такая модель вычислений поощряет неупорядоченное, параллельное выполнение, однако до недавнего времени эта возможность являлась скорее потенциальной энергией, чем достигнутой целью.
Наконец был разработан язык Erlang. Он начал свой путь в конце 1980-х как предметно-ориентированный встроенный язык приложений для телефонии. Предметная область, предполагая десятки тысяч программ, одновременно запущенных на одной машине, заставляла отдать предпочтение обмену сообщениями, когда информация передается в стиле «выстрелил – забыл». Аппаратное обеспечение и операционные системы по большей части не были оптимизированы для таких нагрузок, но Erlang изначально запускался на специализированной платформе. В результате получился язык, оригинальным образом сочетающий нечистый функциональный стиль, серьезные возможности для параллельных вычислений и стойкое предпочтение обмена сообщениями (никакого разделения памяти!).
Перенесемся в 2010-е. Сегодня даже у средних машин больше одного процессора, а главная задача десятилетия – уместить на кристалле как можно больше ЦПУ. Отсюда и последствия, самое важное из которых – конец монолитной разделяемой памяти.
С одним разделяемым по времени ЦПУ связана одна подсистема памяти – с буферами, несколькими уровнями кэшей, все по полной программе. Независимо от того, как ЦПУ управляет разделением времени, чтение и запись проходят по одному и тому же маршруту, а потому видение памяти у разных потоков остается когерентным. Несколько взаимосвязанных ЦПУ, напротив, не могут позволить себе разделять подсистему кэша: такой кэш потребовал бы мультипортового доступа (что дорого и слабо масштабируемо), и его было бы трудно разместить в непосредственной близости ко всем ЦПУ сразу. Вот почему практически все современные ЦПУ производятся со своей кэш-памятью, предназначенной лишь для их собственных нужд. Производительность мультипроцессорной системы зависит главным образом от аппаратного обеспечения и протоколов, соединяющих комплексы ЦПУ+кэш.
Несколько кэшей превращают разделение данных между потоками в чертовски сложную задачу. Теперь операции чтения и записи в разных потоках могут затрагивать разные кэши, поэтому сделать так, чтобы один поток делился данными с другим, стало сложнее, чем раньше.
На самом деле, этот процесс превращается в своего рода обмен сообщениями (Что иронично, поскольку во времена классической многопоточности разделение памяти было быстрее, а обмен сообщениями – медленнее): в каждом случае такого разделения между подсистемами кэшей должно иметь место что-то вроде рукопожатия, обеспечивающего попадание разделяемых данных от последнего записавшего потока к читающему потоку, а также в основную память.
Протоколы синхронизации кэшей добавляют к сюжету еще один поворот (хотя и без него все было достаточно лихо закручено): они воспринимают данные только блоками, не предусматривая чтение и запись отдельных слов. То есть общающиеся друг с другом процессы «не помнят» точный порядок, в котором записывались данные, что приводит к парадоксальному поведению, которое не поддается разумному объяснению и противоречит здравому смыслу: один поток записывает x, а затем y, и в некоторый промежуток времени другой поток видит новое y, но старое x. Такие нарушения причинно-следственных связей слабо вписываются в общую модель классической многопоточности. Даже наиболее сведущим в классической многопоточности программистам невероятно трудно адаптировать свой стиль и шаблоны программирования к новым архитектурам памяти.
for (нога : сороконожка) {
нога.поднять();
}
Все проще :)
сороконожка.форичМ(нога=>нога.поднять());(Здесь М означает монаду)
поднимание_ног [] = []
поднимание_ног(нога:ноги) = поднять(нога) : поднимание_ног[ноги]
поднимание_ног(сороконожка)
Я уверен, вы потратите много минут (не самых приятных), переписывая данный пример в императивном стиле, уместив код в две строки, и при этом, сохранив читаемость.
>>> def plus1(lst):
... return [x+1 for x in lst]
...
>>> plus1([1,2,3])
[2, 3, 4]
>>>
irb> def plus1(lst)
irb> lst.map {|x, obj| x+1}
irb> end
=> :plus1
irb> plus1([1,2,3])
=> [2, 3, 4]
template<typename T>
auto plus1(const vector<T> &lst)
{
auto mod = lst;
transform(mod.begin(), mod.end(), mod.begin(),
[](auto &x) {
return x+1;
});
return mod;
}
template<typename T>
auto plus1(const vector<T> lst)
{
transform(lst.begin(), lst.end(), lst.begin(), [](auto &x) { return x+1; });
return lst;
}
public static int[] incrementArray(int... array) {
for(int i = 0; i < array.length; array[i] += 1, i++) {}
return array;
}
guess x = "Ooops, try again."
guess _ = "Ooops, try again."
plus1 [] = []
plus1 (x:xs) = x + 1 : plus1 xs
plus1 = map (+1)
5.11.1.1. «Чист тот, кто чисто поступает»
По традиции функциональные языки запрещают абсолютно любые изменения, чтобы программа могла называться чистой. D ослабляет это ограничение, разрешая функциям изменять собственное локальное
и временное состояние. Таким образом, даже если внутри функции есть изменения, для окружающего кода она все еще непогрешима.
Посмотрим, как работает это допущение. В качестве примера возьмем наивную реализацию функции Фибоначчи в функциональном стиле:
ulong fib(uint n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
Ни один преподаватель программирования никогда не должен учить реализовывать расчет чисел Фибоначчи таким способом. Чтобы вычислить результат, функции fib требуется экспоненциальное время, поэтому все, чему она может научить, – это пренебрежение сложностью и ценой вычислений, лозунг «небрежно, зато находчиво» и спортивный стиль вождения. Хотите знать, чем плох экспоненциальный порядок? Вызовы fib(10) и fib(20) на современной машине не займут много времени, но вызов fib(50) обрабатывается уже 19 минут. Вполне вероятно, что вычисление fib(1000) переживет человечество.
Хорошо, но как выглядит «правильная» функциональная реализация Фибоначчи?
ulong fib(uint n) { ulong iter(uint i, ulong fib_1, ulong fib_2) { return i == n ? fib_2 : iter(i + 1, fib_1 + fib_2, fib_1); } return iter(0, 1, 0); }
Переработанная версия вычисляет fib(50) практически мгновенно. Эта реализация требует для выполнения O(n) времени, поскольку оптимизация хвостовой рекурсии (см. раздел 1.4.2) позволяет уменьшить сложность вычислений. (Стоит отметить, что для расчета чисел Фибоначчи существуют и алгоритмы с временем выполнения O(log n)).
Проблема в том, что новая функция fib как бы утратила былое великолепие. Особенность переработанной реализации – две переменные состояния, маскирующиеся под параметры функции, и вполне можно было с чистой совестью написать явный цикл, который зачем-то был закамуфлирован функцией iter:
ulong fib(uint n) { ulong fib_1 = 1, fib_2 = 0; foreach (i; 0 .. n) { auto t = fib_1; fib_1 += fib_2; fib_2 = t; } return fib_2; }
К сожалению, это уже не функциональный стиль. Только посмотрите на все эти изменения, происходящие в цикле. Один неверный шаг – и с вершин математической чистоты мы скатились к неискушенности чумазых низов.
Но подумав немного, мы увидим, что итеративная функция fib не такая уж чумазая. Если принять ее за черный ящик, то можно заметить, что при одних и тех же аргументах функция fib всегда возвращает один
и тот же результат, а ведь «красив тот, кто красиво поступает». Тот факт, что она использует локальное изменение состояния, делает ее менее функциональной по букве, но не по духу. Продолжая эту мысль, приходим к очень интересному выводу: пока изменяемое состояние внутри функции остается полностью временным (то есть хранит данные в стеке) и локальным (то есть не передается по ссылке другим функциям, которые могут его нарушить), эту функцию можно считать чистой.
Вот как D определяет функциональную чистоту: в реализации чистой функции разрешается использовать изменения, если они временные и локальные. Сигнатуру такой функции можно снабдить ключевым словом pure, и компилятор без помех скомпилирует этот код:
pure ulong fib(uint n) { ... // Итеративная реализация }
Принятые в D допущения, смягчающие математическое понятие чистоты, очень полезны, поскольку позволяют взять лучшее из двух миров: железные гарантии функциональной чистоты и удобную реализацию
(если код с изменениями более предпочтителен).
fib n = fibs !! n
where fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
var add = function(a, b){ return a + b }
Вы только что создали анонимную функцию, которая получает a и b и возвращает a + b в переменную add.
var added = (function(a, b){ return a + b })(1, 2)
1) ООП в императивном стиле — нарезать мир на плоскости последовательности состояний, и подавать их последовательно на вход простого преобразователя (вашей программы) состояний; 2) ООП в функциональном стиле — нарезается мир на последовательность преобразований, которые и применяются последовательно друг к другу, а потом этот клубок исполняется на входном состоянии (если бы в haskell был бы параллельный исполнитель, то можно было бы натравливать на входное состояние много ядер [что-то вроде fork-map-reduce], и си код стал бы тормазнутым из-за немасштабируемости).
Как эти термины («параллельность» и «конкурентность») звучат на английском?
модель не готова к многопоточности (concurrency) и параллельности (parallelism)
Функциональное программирование должно стать вашим приоритетом №1 в 2015 году