Сила функционального стиля зависит от гарантии , что обе функции принимают значение как input и возвращают значение как output . Если вывод функции не является полностью определенным, любой потребитель вывода нашей функции подвержен потенциально неопределенному поведению. Нулевые проверки утомляют писать, а исключения во время выполнения являются мигренью; мы можем избежать обоих, придерживаясь функциональной дисциплины.
Проблема, представленная в вашем вопросе, является нетривиальной. Данные для извлечения глубоко вложены, и для доступа к компонентам адреса требуется эксцентричный поиск и сопоставление. Чтобы начать писать наше преобразование, мы должны полностью определить домен нашей функции (входной) и кодомен (выходной).
Домен прост: входные данные в вашем вопросе являются объектами, поэтому наше преобразование должно дать действительный результат для всех объектов. Кодомен более специфичен - поскольку преобразование может завершиться с ошибкой любым количеством способов, наша функция будет возвращать либо действительный объект результата, либо , либо . Ничего.
В качестве сигнатуры типа вот как это выглядит -
type Result =
{ latitude: Number
, longitude: Number
, city: String
, zipCode: String
, streetName: String
, streetNumber: String
}
transform : Object -> Maybe Result
Чтобы выразить это простыми словами, при наличии правильных входных данных, наш transform
вернет действительный результат, например -
Just { latitude: 1, longitude: 2, city: "a", zipCode: "b", streetName: "c", streetNumber: "d" }
Когда даны неверные данные, наш transform
ничего не вернет -
Nothing
Никакое другое возвращаемое значение невозможно. Это означает, что наша функция гарантирует, что она не вернет частичный или разреженный результат, например -
{ latitude: 1, longitude: 2, city: undefined, zipCode: "b", streetName: "c", streetNumber: undefined }
Функциональная дисциплина также говорит, что наша функция не должна иметь побочных эффектов, поэтому наше преобразование также должно гарантировать, что она не выдаст ошибку, такую как -
TypeError: cannot read property "location" of undefined
TypeError: data.reduce is not a function
Другие ответы в этой теме не принимают такие меры предосторожности, и они выдают ошибки или дают разреженные результаты, когда входные данные искажены. Наш дисциплинированный подход позволит избежать этих ловушек, гарантируя, что любому потребителю вашей функции transform
не придется сталкиваться с нулевыми проверками или перехватывать потенциальные ошибки времени выполнения.
В основе вашей проблемы мы имеем дело со многими потенциальными значениями. Мы доберемся до пакета data.maybe , который обеспечивает:
Структура для значений, которые могут отсутствовать, или для вычислений, которые могут не работать. Maybe(a)
явно моделирует эффекты, которые неявно присутствуют в типах Nullable
, поэтому не имеет никаких проблем, связанных с использованием null
или undefined
- например, NullPointerException
или TypeError
.
Звучит как хорошая подгонка. Мы начнем с наброска кода и размахиваем руками в воздухе. Давайте представим, что у нас есть getAddress
функция, которая принимает String
, а Object
и , может быть возвращает String
-
// getAddress : String -> Object -> Maybe String
Мы начинаем писать transform
...
const { Just } =
require ("data.maybe")
// transform : Object -> Maybe Result
const transform = (data = {}) =>
getAddress ("locality", data)
.chain
( city =>
getAddress ("postal_code", data)
.chain
( zipCode =>
getAddress ("route", data)
.chain
( streetName =>
Just ({ city, zipCode, streetName })
)
)
)
transform (data)
// Just {city: "Pyrmont", zipCode: "2009", streetName: "Pirrama Road"}
transform ({})
// Nothing
Хорошо, yikes. Мы даже не закончили, и эти вложенные .chain
звонки - полный беспорядок! Если вы посмотрите внимательно, здесь есть простой шаблон. Функциональная дисциплина говорит, что когда вы видите шаблон, вы должны аннотация ; это слово ботаник, означающее сделать функцию .
Прежде чем мы углубимся в ад .chain
, давайте рассмотрим более обобщенный подход. Я должен найти шесть (6) возможных значений в глубоко вложенном объекте, и если я смогу получить все из них, я хочу построить значение Result
-
// getAddress : String -> Object -> Maybe String
// getLocation : String -> Object -> Maybe Number
const { lift } =
require ("ramda")
// make : (Number, Number, String, String, String, String) -> Result
const make = (latitude, longitude, city, zipCode, streetName, streetNumber) =>
({ latitude, longitude, city, zipCode, streetName, streetNumber })
// transform : Object -> Maybe Result
const transform = (o = {}) =>
lift (make)
( getLocation ("lat", o)
, getLocation ("lng", o)
, getAddress ("locality", o)
, getAddress ("postal_code", o)
, getAddress ("route", o)
, getAddress ("street_number", o)
)
transform (data)
// Just {latitude: -33.866651, longitude: 151.195827, city: "Pyrmont", zipCode: "2009", streetName: "Pirrama Road", …}
transform ({})
// Nothing
Правильность восстановлена. Выше мы пишем простую функцию make
, которая принимает шесть (6) аргументов для построения Result
. Используя lift
, мы можем применить make
в контексте из Maybe
, отправив значения Maybe в качестве аргументов. Однако, если любое значение равно Nothing
, в результате мы ничего не получим, и make
не будет применено.
Большая часть тяжелой работы уже выполнена здесь. Нам просто нужно завершить реализацию getAddress
и getLocation
. Мы начнем с getLocation
, который является более простым из двух -
// safeProp : String -> Object -> Maybe a
// getLocation : String -> Object -> Maybe Number
const getLocation = (type = "", o = {}) =>
safeProp ("geometry", o)
.chain (safeProp ("location"))
.chain (safeProp (type))
getLocation ("lat", data)
// Just {value: -33.866651}
getLocation ("lng", data)
// Just {value: 151.195827}
getLocation ("foo", data)
// Nothing
У нас не было safeProp
до того, как мы начали, но мы облегчаем себе жизнь, изобретая удобство по мере продвижения вперед. Функциональная дисциплина говорит, что функции должны быть простыми и выполнять одну задачу. Такие функции легче писать, читать, тестировать и поддерживать. Они имеют дополнительное преимущество, заключающееся в том, что они могут быть составлены и более пригодны для повторного использования в других областях вашей программы. Кроме того, когда функция имеет имя , она позволяет нам кодировать наши намерения более напрямую - getLocation
- это последовательность safeProp
поисков - почти никакая другая интерпретация функции невозможна.
Может показаться раздражающим, что в каждой части этого ответа я раскрываю другую основную зависимость, но это намеренно. Мы будем фокусироваться на большой картине, увеличивая маленькие части, только когда это станет необходимым. getAddress
значительно сложнее реализовать из-за неупорядоченного списка компонентов, который наша функция должна просмотреть, чтобы найти конкретный компонент адреса. Не удивляйтесь, если мы добавим больше функций по мере продвижения -
// safeProp : String -> Object -> Maybe a
// safeFind : (a -> Boolean) -> [ a ] -> Maybe a
const { includes } =
require ("ramda")
// getAddress : String -> Object -> Maybe String
const getAddress = (type = "", o = {}) =>
safeProp ("address_components", o)
.chain
( safeFind
( o =>
safeProp ("types", o)
.map (includes (type))
.getOrElse (false)
)
)
.chain (safeProp ("long_name"))
Иногда скомбинирование множества крошечных функций с использованием pipe
может доставить больше хлопот, чем оно того стоит. Конечно, синтаксис без точки может быть достигнут, но сложные последовательности бесчисленных служебных функций мало что делают для , говоря , что на самом деле должна делать программа. Когда вы прочитаете это pipe
через 3 месяца, вспомните ли вы о своих намерениях?
Напротив, и getLocation
, и getAddress
просты и понятны. Они не бесполезны, но они сообщают читателю, какую работу предполагается выполнить. Кроме того, домен и кодомен определены в total , что означает, что наш transform
может быть составлен с любой другой программой и гарантированно будет работать. Хорошо, давайте раскроем остальные зависимости -
const Maybe =
require ("data.maybe")
const { Nothing, fromNullable } =
Maybe
const { identity, curryN, find } =
require ("ramda")
// safeProp : String -> Object -> Maybe a
const safeProp =
curryN
( 2
, (p = "", o = {}) =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
)
// safeFind : (a -> Boolean) -> [ a ] -> Maybe a
const safeFind =
curryN
( 2
, (test = identity, xs = []) =>
fromNullable (find (test, xs))
)
Выше curryN
требуется, потому что эти функции имеют аргументы по умолчанию. Это компромисс в пользу функции, которая обеспечивает лучшую самостоятельную документацию. Более традиционный curry
может использоваться, если аргументы по умолчанию удалены.
Итак, давайте посмотрим, как работает наша функция. Если введенные данные верны, мы получим ожидаемый результат -
transform (data) .getOrElse ("invalid input")
// { latitude: -33.866651
// , longitude: 151.195827
// , city: "Pyrmont"
// , zipCode: "2009"
// , streetName: "Pirrama Road"
// , streetNumber: "48"
// }
И поскольку наш transform
возвращает значение Maybe, мы можем легко восстановить, если предоставлен неправильный ввод -
transform ({ bad: "input" }) .getOrElse ("invalid input")
// "invalid input"
Запустите эту программу на repl.it до см. Результаты .
Надеюсь, преимущества этого подхода очевидны. Мало того, что мы получили более надежный и надежный transform
, его было легко писать благодаря высокоуровневым абстракциям, таким как Maybe, safeProp
и safeFind
.
И давайте подумаем об этих больших pipe
композициях, прежде чем мы расстанемся. Причина, по которой они иногда ломаются, заключается в том, что не все функции в библиотеке Ramda являются total - некоторые из них возвращают ненулевое значение, undefined
. Например, head
может потенциально возвратить undefined
, и следующая функция в конвейере получит undefined
в качестве ввода. Как только undefined
заражает ваш трубопровод, все гарантии безопасности теряются. С другой стороны, используя структуру данных, специально разработанную для обработки значений, допускающих обнуляемость, мы снимаем сложность и в то же время предоставляем гарантии.
Развивая эту концепцию, мы могли бы найти библиотеку Decoder
или предоставить нашу собственную. Целью этого было бы укрепление наших намерений в общем модуле. getLocation
и getAddress
- это пользовательские помощники, которые мы использовали, чтобы сделать возможным transform
, но в более общем смысле это форма декодера, поэтому нам помогает думать об этом таким образом. Кроме того, структура данных декодера может обеспечить лучшую обратную связь при возникновении ошибок - то есть вместо Nothing
, который только сигнализирует нам о том, что значение не может быть получено, мы можем приложить причину или другую информацию, касающуюся конкретного сбоя. Пакет декодеров npm заслуживает внимания.
см. ШотландецОтвет для решения этой проблемы другим способом с использованием высокоуровневой абстракции под названием lens .Тем не менее, обратите внимание, что функция нечиста - необходимы дополнительные меры предосторожности, чтобы функция не генерировала ошибки времени выполнения для искаженных входных данных.
Комментарий Скотта представляет допустимый сценарий, в котором вы можете хотеть разреженныйрезультат.Мы можем переопределить наш тип Result
как -
type Result =
{ latitude: Maybe Number
, longitude: Maybe Number
, city: String
, zipCode: String
, streetName: String
, streetNumber: String
}
Конечно, это означает, что нам придется переопределить transform
для построения этой новой структуры.Самое главное, что потребители Result
знают, чего ожидать, так как кодомен хорошо определен.
Другой вариант - сохранить исходный тип Result
, но указать значение по умолчанию, когда значения широты или долготыне может быть найден -
const transform = (o = {}) =>
lift (make)
( getLocation ("lat", o)
.orElse (_ => Just (0))
, getLocation ("lng", o)
.orElse (_ => Just (0))
, getAddress ("locality", o)
, getAddress ("postal_code", o)
, getAddress ("route", o)
, getAddress ("street_number", o)
)
Каждое поле в Result
может быть необязательным, если вы того пожелаете.В любом случае, мы должны четко определить домен и кодомен и убедиться, что наш transform
выполняет свое обещание.Это единственный способ, которым можно безопасно включить в большую программу.