«Почему 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 бесплатно вместе с вышеупомянутым множеством других функций.