Как написать идиоматический 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"))))