«Почему map
вернул список, когда я дал ему вектор?»
(seq '(1 2 3)) => (1 2 3)
(seq [1 2 3]) => (1 2 3)
(seq #{1 2 3}) => (1 2 3)
(seq {:name "Bill Compton" :occupation "Dead mopey guy"}) => ([:name "Bill Compton"] [:occupation "Dead mopey guy"])
«Каким образом reduce
рассматривает мою карту как список векторов?»
Чтобы понять программирование с точки зрения абстракций, давайте сравним Clojure с языком, который не был построен с учетом этого принципа: Emacs Lisp (elisp). В elisp вы можете использовать функцию mapcar
для создания нового списка, что аналогично тому, как вы используете map
в Clojure. Однако, если вы хотите отобразить хэш-карту (аналогичную структуре данных карты Clojure) в elisp, вам нужно будет использовать функцию maphash
, тогда как в Clojure вы все равно можете просто использовать map
. Другими словами, elisp использует две разные функции, зависящие от структур данных, для реализации операции map, а Clojure использует только одну. Вы также можете вызвать reduce
на карте в Clojure, тогда как elisp не предоставляет функции для сокращения хеш-карты.
Причина в том, что Clojure определяет функции map
и reduce
в терминах абстракции последовательности, а не в терминах конкретных структур данных. Пока структура данных реагирует на основные операции последовательности (функции first
, rest
и cons
), она будет бесплатно работать с map
, reduce
и множеством других функций последовательности. Это то, что Clojurists подразумевают под программированием на абстракции, и это центральный принцип философии Clojure.
map
на Clojure не заботится о том, как реализованы списки, векторы, наборы и карты. Его заботит только то, может ли он выполнять над ними операции последовательности.
Дело в том, чтобы оценить различие между абстракцией seq в Clojure и конкретной реализацией связанного списка. Не имеет значения, как реализована конкретная структура данных: когда дело доходит до использования функций seq в структуре данных, все, что Clojure спрашивает: «Могу ли я first
, rest
и cons
это?» Если ответ положительный, вы можете использовать библиотеку seq с этой структурой данных.
Давайте попробуем построить эту абстракцию последовательности с помощью JavaScript.
var node3 = { value: "last", next: null };
var node2 = { value: "middle", next: node3 }; var node1 = { value: "first", next: node2 };
Это создаст связанный список с next
является нулевым, потому что это последний узел.
В связанном списке можно выполнять три основные функции: first
, rest
и cons
. first
возвращает значение для запрошенного узла, rest
возвращает оставшиеся значения после запрошенного узла, а cons
(некоторые люди называют это consing) добавляет новый узел с заданным значением в начало списка. После того, как они будут реализованы, вы можете реализовать map
, reduce
, filter
и другие функции seq поверх них.
В следующем коде показано, как мы могли бы реализовать и использовать first
, rest
и cons
в нашем примере узла JavaScript, а также как использовать их для возврата определенных узлов и создания нового списка.
var first = function(node) {
return node.value;
};
var rest = function(node) {
return node.next;
};
var cons = function(newValue, node) {
return {
value: newValue,
next: node
};
};
first(node1); => "first"
first(rest(node1)); => "middle"
first(rest(rest(node1))); => "last"
var node0 = cons("new first", node1);
first(node0); => "new first"
first(rest(node0)); => "first"
С помощью этого кода мы теперь можем реализовать map
с first
, rest
и cons
.
var map = function (list, transform) {
if (list === null) {
return null;
} else {
return cons(transform(first(list)), map(rest(list), transform));
}
}
Эта функция преобразует первый элемент списка, а затем снова вызывает себя для остальной части списка, пока не достигнет конца (ноль). Давайте посмотрим на это в действии! В этом примере вы сопоставляете список, который начинается с node1
, возвращая новый список, в котором строка " mapped!"
добавляется к значению каждого узла. Затем вы используете first
, чтобы вернуть значение первого узла:
first(
map(node1, function (val) { return val + " mapped!"})
); => "first mapped!"
Итак, вот что замечательно: поскольку map
полностью реализован в терминах cons
, first
и rest
, вы можете передать ему любую структуру данных, и он будет работать, пока cons
, first
и rest
работают с этой структурой данных.
Этот фрагмент кода определяет first
, rest
и cons
в терминах функций массива JavaScript.
var first = function (array) {
return array[0];
}
var rest = function (array) {
var sliced = array.slice(1, array.length);
if (sliced.length == 0) {
return null;
} else {
return sliced;
}
}
var cons = function (newValue, array) {
return [newValue].concat(array);
}
var list = ["Transylvania", "Forks, WA"];
map(list, function (val) { return val + " mapped!"}) => ["Transylvania mapped!", "Forks, WA mapped!"]
Пока map
ссылается на first
, rest
и cons
, теперь он работает с массивом. Итак, если вы можете просто реализовать first
, rest
и cons
, вы получите map
бесплатно вместе с вышеупомянутым множеством других функций.