Одно поле слишком много

Иногда не удается найти mapN с достаточно высоким N

я ›Ааааа, только не снова!

ват ›Ненавижу звук разочарования по утрам. Что случилось?

я ›Что случилось? Я тебе скажу, что случилось! Вот что не так:

type alias Person =
    { firstName: String
    , lastName: String
    , age: Int
    , email: String
    , phone: String
    , hairColor: String
    , eyeColor: String
    , favoriteFood: String
    , favoriteDrink: String
    }

wat ›Это похоже на очень хороший псевдоним типа (если немного шире). Что насчет этого?

я ›Раньше у него было на два поля меньше (favoriteFood и favoriteDrink - поздние добавления), поэтому, когда я хотел декодировать его из JSON, я мог просто использовать Decode.map7 и покончить с этим:

type alias Person =
    { firstName: String
    , lastName: String
    , age: Int
    , email: String
    , phone: String
    , hairColor: String
    , eyeColor: String
    }
personDecoder : Decoder Person
personDecoder =
    Decode.map7 Person
        (field "firstName" string)
        (field "lastName" string)
        (field "age" int)
        (field "email" string)
        (field "phone" string)
        (field "hairColor" string)
        (field "eyeColor" string)

ват ›Так оно и было. Теперь тебе нужен map9, я думаю ...

меня ›… которого, как оказалось, не существует. Или, по крайней мере, не в Json.Decode.

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

я ›Ну, конечно, но… разве нет… функционального способа сделать это без необходимости рефакторинга всего моего кода, чтобы освободить место для новых полей?…

ват ›Вы каким-то образом пытаетесь заставить меня раскрыть более запретные, эзотерические знания глубокой, темной информатики?

я да?

ват ›Ну, это… очевидно, работает. Вот взгляните на эту интригующую новую функцию:

-- (from Json.Decode.Extra)
andMap : Decoder a -> Decoder (a -> b) -> Decoder b

я ›Погодите ... разве я не знаю это откуда-то? ... Имя знакомое ...

wat ›Вы можете найти это похоже на Json.Decode.map, если переключите параметры (и немного прищурились):

-- (from Json.Decode.Extra)
andMap : Decoder a -> Decoder (a -> b) -> Decoder b
-- (from Json.Decode)
map : (a -> b) -> Decoder a -> Decoder b

я ›Ага ... но это как если бы мы поменяли местами функцию преобразования с декодером ... и почему-то обернули ее в Decoder. Зачем нам заключать функцию в Decoder?

ват ›Скажем сейчас, это не функция. Допустим, это «пустой b», для заполнения которого требуется только a.

я ›Хм… ладно. Итак, это конструктор разновидностей?

ват ›Конечно. Теперь предположим, что andMap похож на роботизированную руку, которая может вытащить a из Decoder a и поместить это a внутрь нашего «пустого b», чтобы получился полный b.

я ›Понял.

wat ›Давайте посмотрим, как это работает, на простейшем примере - декодере, который берет строковое поле и передает его очень простому человеку:

type alias Person =
    { firstName: String }
personDecoder : Decoder Person
personDecoder =
    andMap
        (field "firstName" string) -- : Decoder String
        (succeed Person)           -- : Decoder (String -> Person)

я ›Звучит ... бесполезно сложно. Но да, вы берете строковый декодер, который извлекает строку из поля firstName, andMap каким-то образом разворачивает ее, а затем передает ее конструктору Person в своем всегда последующем декодере. И я предполагаю, что когда Decoder String выходит из строя, мы получаем Decoder Person просто неисправный декодер. Думаю, я понял.

ват ›Теперь давайте запишем это более конвейерно:

personDecoder =
    succeed Person
        |> andMap (field "firstName" string)

я ›Хорошо, это то же самое, только мы передаем конструктору в его всегда преуспевающем декодере andMap, используя (|>). Ну и что?

ват ›А теперь давайте добавим в Person еще одно поле, которое тоже нужно расшифровать:

type alias Person =
    { firstName: String
    , lastName: String
    }
personDecoder : Decoder Person
personDecoder =
    succeed Person
        |> andMap (field "firstName" string)
        |> andMap (field "lastName" string)

ват ›Не требуется mapN 😉

я ›Ой. Это похоже на то, как если бы ваши роботизированные руки были связаны вместе, чтобы сформировать некую сборочную линию, в которой элементы один за другим помещаются в наш конструктор… Великолепно!

ват ›Да! Давайте вернемся к нашему определению andMap, не так ли?

andMap : Decoder a -> Decoder (a -> b) -> Decoder b

ват ›Итак, у нас есть этот Person : String -> String -> Person конструктор, который хочет, чтобы две строки строили его Person значение, верно? И у нас есть два декодера, которые могут декодировать строки. Но эти декодеры могут выйти из строя. Итак, сначала мы превращаем конструктор в успешный Decoder и используем andMap, чтобы объединить их с операцией декодирования:

succeed Person -- this is a Decoder (String -> String -> Person)
  |> andMap (field "firstName" string)
      -- the result is a Decoder (String -> Person)
      -- (the first String is provided by the first decoder)
  |> andMap (field "lastName" string)
      -- the result is a Decoder Person
      -- (now it can decode a whole Person :)

я ›Дай мне попробовать и…

type alias Person =
    { firstName: String
    , lastName: String
    , age: Int
    , email: String
    , phone: String
    , hairColor: String
    , eyeColor: String
    , favoriteFood: String
    , favoriteDrink: String
    }
personDecoder : Decoder Person
personDecoder =
    succeed Person
        |> andMap (field "firstName" string)
        |> andMap (field "lastName" string)
        |> andMap (field "age" int)
        |> andMap (field "email" string)
        |> andMap (field "phone" string)
        |> andMap (field "hairColor" string)
        |> andMap (field "eyeColor" string)
        |> andMap (field "favoriteFood" string)
        |> andMap (field "favoriteDrink" string)

я ›😢 это так красиво…

ват ›И знаете что?… Так же, как map - это сверхважная функция Функтора, а andThen Монада, есть имя для вещей, над которыми вы можете andMap

я ›Позже. Позвольте мне насладиться этим чувством.

wat ›… и это имя - Applicative Functor. Мы скоро попробуем обобщить . Пока.

[1] не слушайте этих людей; ваши большие плоские предметы в порядке, если вы думаете, что они в порядке; вы должны группироваться, когда это имеет для вас смысл, а не для того, чтобы угодить кому-то другому. Кроме того, те же люди, как правило, все равно находят ваши объекты слишком глубоко вложенными.