Как я могу улучшить простоту работы с JSON в Haskell? - PullRequest
4 голосов
/ 04 апреля 2019

Haskell стал полезным в качестве веб-языка (спасибо Servant!), И все же JSON по-прежнему настолько болезнен для меня, что я, должно быть, делаю что-то не так (?)

Я слышал, что JSON упоминается какДостаточно больно, и ответы, которые я слышал, вращаются вокруг «используйте PureScript», «дождитесь суб / строкового набора», «используйте эзотерику, как винил», «Aeson + просто справляется со взрывом типов данных котельной пластины».

Как (несправедливый) ориентир, я действительно наслаждаюсь легкостью JSON-истории Clojure (конечно, это динамический язык, и у него есть свои компромиссы, из-за которых я все еще предпочитаю Haskell).

Вот пример, на который я смотрел в течение часа.

{
    "access_token": "xxx",
    "batch": [
        {"method":"GET", "name":"oldmsg", "relative_url": "<MESSAGE-ID>?fields=from,message,id"},
        {"method":"GET", "name":"imp", "relative_url": "{result=oldmsg:$.from.id}?fields=impersonate_token"},
        {"method":"POST", "name":"newmsg", "relative_url": "<GROUP-ID>/feed?access_token={result=imp:$.impersonate_token}", "body":"message={result=oldmsg:$.message}"},
        {"method":"POST", "name":"oldcomment", "relative_url": "{result=oldmsg:$.id}/comments", "body":"message=Post moved to https://workplace.facebook.com/{result=newmsg:$.id}"},
        {"method":"POST", "name":"newcomment", "relative_url": "{result=newmsg:$.id}/comments", "body":"message=Post moved from https://workplace.facebook.com/{result=oldmsg:$.id}"},
    ]
}

Мне нужно отправить это на рабочее место FB, которое скопирует сообщение в новую группу и прокомментирует ссылку на обоих,связывание друг с другом.

Моя первая попытка выглядела примерно так:

data BatchReq = BatchReq {
  method :: Text
  , name :: Text
  , relativeUrl :: Text
  , body :: Maybe Text
  }

data BatchReqs = BatchReqs {
  accessToken :: Text
  , batch :: [BatchReq]
  }

softMove tok msgId= BatchReqs tok [
  BatchReq "GET" "oldmsg" (msgId `append` "?fields=from,message,id") Nothing
  ...
  ]

Это ужасно жестко, и иметь дело со Maybe повсюду неудобно.Nothing - это JSON null?Или поле должно отсутствовать?Затем я беспокоился о получении экземпляров Aeson и должен был выяснить, как конвертировать, например, relativeUrl в relative_url.Затем я добавил конечную точку, и теперь у меня есть конфликт имен.DuplicateRecordFields!Но подождите, это вызывает так много проблем в других местах.Поэтому обновите тип данных, чтобы использовать, например, batchReqRelativeUrl, и удалите его при получении экземпляров, используя Typeable s и Proxy s.Затем мне нужно было добавить конечные точки и / или поменять форму этого жесткого типа данных, для которого я добавил больше точек данных, стараясь не допустить, чтобы «тирания небольших различий» слишком раздула мои типы данных.

При этомЯ был в основном потребляющим JSON, поэтому решил, что "динамическим" будет использование lens es.Итак, чтобы развернуть поле JSON, содержащее идентификатор группы, я сделал:

filteredBy :: (Choice p, Applicative f) =>  (a -> Bool) -> Getting (Data.Monoid.First a) s a -> Optic' p f s s
filteredBy cond lens = filtered (\x -> maybe False cond (x ^? lens))

-- the group to which to move the message
groupId :: AsValue s => s -> AppM Text
groupId json  = maybe (error500 "couldn't find group id in json.")
                pure (json ^? l)
  where l = changeValue . key "message_tags" . values . filteredBy (== "group") (key "type") . key "id" . _String

Это довольно тяжело для доступа к полям.Но мне также нужно генерировать полезные нагрузки, и я не достаточно опытен, чтобы увидеть, как линзы будут хороши для этого.Обращаясь к мотивирующему пакетному запросу, я придумал «динамический» способ написания этих полезных нагрузок.Это можно упростить с помощью помощника fns, но я даже не уверен, насколько лучше это получится.

softMove :: Text -> Text -> Text -> Value
softMove accessToken msgId groupId = object [
  "access_token" .= accessToken
  , "batch" .= [
        object ["method" .= String "GET", "name" .= String "oldmsg", "relative_url" .= String (msgId `append` "?fields=from,message,id")]
      , object ["method" .= String "GET", "name" .= String "imp", "relative_url" .= String "{result=oldmsg:$.from.id}?fields=impersonate_token"]
      , object ["method" .= String "POST", "name" .= String "newmsg", "relative_url" .= String (groupId `append` "/feed?access_token={result=imp:$.impersonate_token}"), "body" .= String "message={result=oldmsg:$.message}"]
      , object ["method" .= String "POST", "name" .= String "oldcomment", "relative_url" .= String "{result=oldmsg:$.id}/comments", "body" .= String "message=Post moved to https://workplace.facebook.com/{result=newmsg:$.id}"]
      , object ["method" .= String "POST", "name" .= String "newcomment", "relative_url" .= String "{result=newmsg:$.id}/comments", "body" .= String "message=Post moved from https://workplace.facebook.com/{result=oldmsg:$.id}"]
      ]
  ]

Я рассматриваю возможность иметь в коде BLOB-объекты JSON или читать их как файлыи используя Text.Printf для объединения в переменные ...

Я имею в виду, я могу сделать все это так, но, безусловно, хотел бы найти альтернативу.API FB немного уникален тем, что его нельзя представить в виде жесткой структуры данных, как многие API-интерфейсы REST;они называют его своим API-интерфейсом Graph, который более динамичен в использовании, и рассматривать его как жесткий API до сих пор было болезненно.

(Кроме того, благодаря всему сообществу, помогшему мне продвинуться так далеко с Haskell!)

1 Ответ

3 голосов
/ 05 апреля 2019

Обновление: Добавлены некоторые комментарии к "динамической стратегии" внизу.

В подобных ситуациях я использовал односимвольных помощников для хорошего эффекта:

json1 :: Value
json1 = o[ "batch" .=
           [ o[ "method" .= s"GET", "name" .= s"oldmsg",
                   "url" .= s"..." ]
           , o[ "method" .= s"POST", "name" .= s"newmsg",
                   "url" .= s"...", "body" .= s"..." ]
           ]
         ]
  where o = object
        s = String

Обратите внимание, что нестандартный синтаксис (без пробела между односимвольным помощником и аргументом) является преднамеренным. Это сигнал для меня и других читающих мой код, что это технические «аннотации» для удовлетворения проверки типов, а не более обычный вид вызова функции, который на самом деле делает что-то.

Хотя это добавляет небольшой беспорядок, аннотации легко игнорировать при чтении кода. Их также легко забыть при написании кода, но средство проверки типов их ловит, поэтому их легко исправить.

В вашем конкретном случае, я думаю, что некоторые более структурированные помощники делают имеют смысл. Что-то вроде:

softMove :: Text -> Text -> Text -> Value
softMove accessToken msgId groupId = object [
  "access_token" .= accessToken
  , "batch" .= [
        get "oldmsg" (msgId <> "?fields=from,message,id")
      , get "imp" "{result=oldmsg:$.from.id}?fields=impersonate_token"
      , post "newmsg" (groupId <> "...") "..."
      , post "oldcomment" "{result=oldmsg:$.id}/comments" "..."
      , post "newcomment" "{result=newmsg:$.id}/comments" "..."
      ]
  ]
  where get name url = object $ req "GET" name url
        post name url body = object $ req "POST" name url 
                             <> ["body" .= s body]
        req method name url = [ "method" .= s method, "name" .= s name, 
                                "relative_url" .= s url ]
        s = String

Обратите внимание, что вы можете адаптировать эти помощники к конкретному JSON, который вы генерируете в конкретном случае, и определять их локально в предложении where. Вам не нужно выделять какой-то большой кусок инфраструктуры ADT и функций, который охватывает все сценарии использования JSON в вашем коде, как вы могли бы сделать, если бы JSON был более унифицированным по структуре в приложении.

Комментарии к «Динамической стратегии»

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

Боль, с которой вы столкнулись при использовании более традиционного подхода, основанного на ADT, к этой проблеме (жесткость типов, распространение Maybes и «тирания небольших различий») предполагает, что эти типы были плохой моделью по крайней мере, для того, что вы пытались сделать в этом случае. В частности, учитывая, что ваша проблема заключалась в генерировании довольно простых JSON-директив / команд для внешнего API, а не в выполнении множества манипуляций с данными в структурах, которые также происходили чтобы разрешить сериализацию / десериализацию JSON, моделирование данных как ADT на Haskell, вероятно, было излишним.

Мое лучшее предположение заключается в том, что если вы действительно хотите правильно смоделировать FB Workplace API, вы не захотите делать это на уровне JSON. Вместо этого вы бы делали это на более высоком уровне абстракции с типами Message, Comment и Group, и вы в конечном итоге захотите динамически генерировать JSON, потому что ваши типы не будут напрямую отображаться к структурам JSON, ожидаемым API.

Может быть полезно сравнить вашу проблему с генерацией HTML. Сначала рассмотрим шаблоны lucid (blaze) или shakespeare. Если вы посмотрите, как они работают, они не попытаются создать HTML, создав DOM с ADT, такими как data Element = ImgElement ... | BlockquoteElement ..., а затем сериализовав их в HTML. Предположительно авторы решили, что эта абстракция на самом деле не нужна, потому что HTML просто нужно генерировать , а не анализировать . Вместо этого они используют функции (lucid) или квазиквотер (shakespeare) для построения динамической структуры данных, представляющей документ HTML. Выбранная структура является достаточно жесткой, чтобы обеспечить определенные виды валидности (например, правильное соответствие открывающих и закрывающих тегов элементов), но не другие (например, никто не мешает вам прикрепить дочерний элемент <p> в середине элемента <span> ).

Когда вы используете эти пакеты в более крупном веб-приложении, вы моделируете проблемную область на более высоком уровне абстракции, чем элементы HTML, и генерируете HTML в значительной степени динамически, поскольку нет четкого однозначного сопоставления. между типами в модели проблемной области и элементами HTML.

С другой стороны, есть пакет type-of-html, который выполняет моделирование отдельных элементов, поэтому ошибка типа - попытаться вложить <tr> в <td> и так далее.Разработка этих типов, вероятно, потребовала много работы, и в них заложено много негибкости, но компромисс - это совершенно другой уровень безопасности типов.С другой стороны, это выглядит проще для HTML, чем для конкретного привередливого JSON API.

...