Как написать макрос потоков Clojure? - PullRequest
0 голосов
/ 13 февраля 2020

Я пытаюсь написать макрос с использованием failjure/ok->> (https://github.com/adambard/failjure#ok - и-ok- ), а последняя функция в потоке требует выполнения условия. Код выглядит примерно так:

(f/ok->> (function1 param)
         (function2 param1 param 2)
         ...
         ({conditional function here}))

, где, если условие не выполнено, макрос потоков возвращает результат предпоследнего вызова функции. Я попытался написать функцию cond, которая проверила наличие необходимого условия, а затем либо вернула функцию, если условие выполнено, либо просто результат предыдущей функции, но макрос многопоточности, похоже, не передает результат функции в пределах cond, но только сам cond. (неправильный) код выглядел так:

(f/ok->> (function1 param)
         (function2 param1 param 2)
         ...
         (cond (condition?)
             (function_if_passes_condition)
             #(%))

Мне интересно, есть ли чистый способ сделать это правильно. Я полагаю, что можно написать новый макрос многопоточности с такой функциональностью, но до сих пор все мои попытки сделать это не сработали (я не писал defmacro, который реализует макрос многопоточности ранее, и это было довольно сложно, так как я новичок в уроках с 3-х месячным опытом).

1 Ответ

3 голосов
/ 13 февраля 2020

Ваша формулировка проблемы кажется немного расплывчатой, поэтому я решу упрощенную версию проблемы.

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

Давайте напишем предикат помощника и модульный тест:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)   ; <= *** convenience functions! ***
  (:require [clojure.pprint :as pprint]))

(defn century? [x] (zero? (mod x 100)))
(dotest
  (isnt (century? 1399))
  (is   (century? 1300)))

Предположим, мы хотим перевести этот код:

  (check-> 10
    (+ 3)
    (* 100)
    (century?) )

в это:

  (-> 10
    (+ 3)
    (* 100)
    (if (century) ; <= arg goes here
        :pass
        :fail))

Переписать цель немного:

  (let [x (-> 10    ; add a temp variable `x`
            (+ 3)
            (* 100))]
    (if (century? x) ; <= use it here
      :pass
      :fail))

Теперь начнем с функции -impl. Напишите немного, с некоторыми заявлениями для печати. Внимательно обратите внимание на шаблон для использования:

(defn check->-impl
  [args]  ; no `&` 

  (spyx args)     ; <= will print variable name and value to output
))

(defmacro check->
  [& args] ; notice `&`
  (check->-impl args))  ; DO NOT use syntax-quote here

и проведите его с помощью модульного теста. Обязательно следуйте схеме оборачивания аргументов в цитируемом векторе . Это моделирует то, что [& args] делает в выражении defmacro.

(dotest
  (pprint/pprint
    (check->-impl '[10
                    (+ 3)
                    (* 100)
                    (century?)])
    ))

с результатом:

args => [10 (+ 3) (* 100) (century?)]   ; 1 (from spyx)
[10 (+ 3) (* 100) (century?)]           ; 2 (from pprint)

Таким образом, мы видим результат, напечатанный в (1), затем функцию impl возвращает (неизмененный) код в (2). Это ключ. Макрос возвращает модифицированный код . Затем компилятор компилирует измененный код вместо исходного.

Напишите еще немного кода с большим количеством отпечатков:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args) ]
    (spyx all-but-last)        ; (1)
    (spyx last-arg)            ; (2)
))

с результатом

all-but-last => (10 (+ 3) (* 100))    ; from (1)
last-arg     => (century?)            ; from (2)
(century?)                            ; from pprint

Обратите внимание, что произошло , Мы видим наши измененные переменные, но результат также изменился. Напишите еще немного кода:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args)
        cond-expr    (append last-arg 'x)]  ; from tupelo.core
    (spyx cond-expr)
))

cond-expr => [century? x]  ; oops!  need a list, not a vector

Упс! Функция append всегда возвращает вектор. Просто используйте ->list, чтобы преобразовать его в список. Вы также можете набрать (apply list ...).

cond-expr => (century? x)  ; better

Теперь мы можем использовать синтаксическую кавычку для создания кода нашего выходного шаблона:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args)
        cond-expr    (->list (append last-arg 'x))]
    ; template for output code
    `(let [x (-> ~@all-but-last)] ; Note using `~@` eval-splicing
       (if ~cond-expr
         :pass
         :fail))))

с результатом:

(clojure.core/let
 [tst.demo.core/x (clojure.core/-> 10 (+ 3) (* 100))]
 (if (century? x) :pass :fail))

Видите часть tst.demo.core/x? Это проблема. Нам нужно переписать:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args)]
    ; template for output code.  Note all 'let' variables need a `#` suffix for gensym
    `(let [x#           (-> ~@all-but-last) ; re-use pre-existing threading macro
           pred-result# (-> x# ~last-arg)] ; simplest way of getting x# into `last-arg`
       (if pred-result#
         :pass
         :fail))))

ПРИМЕЧАНИЕ. Важно правильно использовать ~ (eval) и ~@ (eval-splicing). Легко ошибиться. Теперь мы получаем

(clojure.core/let
 [x__20331__auto__             (clojure.core/-> 10 (+ 3) (* 100))
  pred-result__20332__auto__   (clojure.core/-> x__20331__auto__ (century?))]
 (if pred-expr__20333__auto__ 
    :pass 
    :fail))

Попробуйте по-настоящему. Разверните аргументы из вектора в кавычках и вызовите макрос вместо функции impl:

  (spyx-pretty :final-result
    (check-> 10
      (+ 3)
      (* 100)
      (century?)))

с выводом:

 :final-result
(check-> 10 (+ 3) (* 100) (century?)) => 
:pass

и напишите несколько модульных тестов:

(dotest
  (is= :pass (check-> 10
               (+ 3)
               (* 100)
               (century?)))
  (is= :fail (check-> 10
               (+ 3)
               (* 101)
               (century?))))

с результатом:

-------------------------------
   Clojure 1.10.1    Java 13
-------------------------------

Testing tst.demo.core

Ran 3 tests containing 4 assertions.
0 failures, 0 errors.

Вы также можете быть заинтересованы в этой книге: Мастеринг макросов Clojure

enter image description here

...