Как написать идиоматический clojure (+ функциональный) код?

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

Написание веб-приложения с использованием compojure, здесь одно из моих действий с контроллером:

(defn create [session params]
  (let [user (user/find-by-email (params :email))]
    (if user
        (if (user/authenticate user (params :password))
            (do (sign-in session user)
                (resp/redirect "/home?signed-in=true"))
            (resp/redirect "/?error=incorrect-password"))
        (let [new-user (user/create params)]
          (sign-in session new-user)
          (resp/redirect "/home?new-user=true")))))

Я пишу это очень императивно. Используя так много let s/if s/do s, я не могу не думать, что я делаю что-то очень не так. Как я могу написать это функционально?

Здесь psuedocode для того, что я пытаюсь сделать

look if user exists
  if user exists, try to sign user in using password provided
    if password is wrong, redirect to "/?error=incorrect-password"
    if password is correct, sign user in and redirect to "/home?signed-in=true"
  else create user, sign user in, and redirect to "/home?new-user=true"

Большое спасибо!

Ответ 1

В if нет ничего нефункционального - это совершенно хорошая, чисто функциональная конструкция, которая условно оценивает выражение. Слишком много if в одном месте может быть предупреждающим знаком, хотя вы должны использовать другую конструкцию (например, cond? Полиморфизм с протоколами? Multimethods? Состав функций более высокого порядка?)

do более сложный: если у вас есть, то это означает, что вы делаете что-то для побочного эффекта, который определенно нефункционально. В вашем случае sign-in и user/create кажутся побочными эффектами преступников.

Что делать с побочными эффектами? Ну, они иногда необходимы, поэтому проблема заключается в том, как ваша структура вашего кода обеспечивает контроль и управление побочными эффектами (в идеале реорганизуется в специальную зону опасности для обработки состояния, так что остальная часть вашего кода может оставаться чистой и чисто функциональной).

В вашем случае вы можете подумать:

  • Передача функции "пользователь/аутентификация" как часть вашего ввода (например, в некоторой форме контекстной карты). Это позволит вам передавать функции проверки подлинности, например, когда вы не хотите использовать реальную базу данных.
  • Вернуть флаг "успешно аутентифицированный" как часть вывода. Это было бы уловлено функцией обработчика более высокого уровня, которая могла бы отвечать за выполнение любых побочных эффектов, связанных с входом в систему.
  • Альтернативно вернуть флаг "новый пользователь" как часть вывода, чтобы функция обработчика также распознала и выполнила любую требуемую настройку пользователя.

Ответ 2

Функциональные стили программирования поощряют использование функций более высокого уровня, таких как карта, сокращение и фильтрация, а также заставляют вас чаще обращаться с неизменяемыми структурами данных. Пока что с вашим кодом ничего не случилось, так как вы не нарушили ни одного правила в функциональном программировании. Тем не менее, вы можете немного улучшить свой код, например, комбинировать let и if using if-let.

Ответ 3

Это в основном выглядит разумно, и поскольку ваши единственные побочные эффекты "необходимы", то есть добавление к сеансу, я бы не назвал это ужасно необходимым. Я бы сделал три изменения, хотя третий вариант является необязательным:

  • В качестве другого предложенного ответа измените пару if/let на один if-let.
  • Используйте (:foo bar) для поиска по ключевым словам на карте, а не (bar :foo). Это просто стандартный способ сделать это.
  • Не беспокойтесь о создании локального для new-user, поскольку вы используете его только один раз; вы можете просто вставить его.

Там другое изменение я бы не сделал, так как я думаю, что это уменьшит читаемость. Однако, это вопрос стиля и суждения, поэтому я упомянул об этом как о чем-то, о чем вы думаете. Обратите внимание, как каждая ветвь лабиринта if заканчивается при вызове resp/redirect: вы можете вывести все эти вызовы на верхний уровень. а затем решить, какие аргументы перейти к нему. В сочетании с другими изменениями он будет выглядеть так:

(defn create [session params]
  (resp/redirect (if-let [user (user/find-by-email (:email params))]
                   (if (user/authenticate user (:password params))
                     (do (sign-in session user)
                         "/home?signed-in=true")
                     "/?error=incorrect-password")
                   (do (sign-in session (user/create params))
                       "/home?new-user=true"))))