🔖ClojureScriptの基礎文法まとめ.

ClojureScript文字列操作

フォーマット文字列

goog.string/formatをつかう. clojure.core/formatは同一機能だからか削除されてる.

(goog.string/format "名前: %s, 年齢: %d" "太郎" 30)

ClojureScript例外

📝Clojure Exception: 例外も参照.

:repl/exception!

REPLセッションが例外によって中断されたことを意味する. REPLの再起動が必要.

Nodejsのグローバルエラーハンドリングで補足

Node.js固有のテクニック. processにアクセスして, 補足されてないエラーを捕まえる.

(defonce node-process (js/require "process"))
 
(.on node-process "uncaughtException"
     (fn [err]
       (tap> err)
       (js/console.error "Caught exception:" err)))
 
(.on node-process "unhandledRejection"
     (fn [reason promise]
       (tap> {:promise promise
              :reason  reason})
       (js/console.error "Unhandled Rejection at:" promise "reason:" reason)))

💡nodejsのPromise catch書き忘れをグローバルエラーハンドラで補足

🆚ClojureScript/JavaScript Interop

Clojureの文法がわかってるとClojureScriptはその部分なので覚えることが少ない. 差分があるとすると, JavsScript互換の文法のみ. なのでここではその知識を整理する.

JavaScript Object

instantiation

#js で生成.

getter

(.-property obj)でJavaScript object属性にアクセス.

(prn (.-width canvas))
(prn (.-height canvas))

📝メソッドチェーンのような属性アクセスはスレッディングマクロが見やすい.

(-> obj .-attr1 .-attr2 .-attr3)

プロパティ名にハイフンを含んでいる場合はClojureScriptはそれを認識できない. agetをつかう.

(aget obj "hoge-huga")

setter

set!をつかうとJavaScript Objectの属性を更新.

(set! (.-width canvas) 500)
(set! (.-height canvas) 500)

static method

((.-method Hoge))
(Hoge.method )

static methodでさらにthis.xxxを呼び出しているとき、thisがnullになる. applyをつかって回避する.

; methodAを正しく呼び出す(applyを使う)
(.apply (.-methodA js/MyClass) js/MyClass #js [1 2])
 
;; methodBを正しく呼び出す
(.apply (.-methodB js/MyClass) js/MyClass #js [3])

<2025-02-18 Tue 18:59> こんなのわかるわけないだろと思ったり.

JSオブジェクト とcljsオブジェクトの変換

関数を利用.

clj->js

ClojureのMapをjsの関数に渡すときは (#js {:hoge 1 :foo 2}) みたいな記法もある. #jsでマップのJS Object生成.

js->clj

keywordize-keysオプションを利用すると, jsonのkeyの文字列はkeywordに変換可能.

(js->clj a :keywordize-keys true)

JS外部ライブラリ利用

js/namespaceを利用してJavsScriptにアクセスする.

(js/alert "hi")
(js/Date)
(js/console.log "hi")

npm packagesのimport

Shadow CLJS User’s Guideに例が沢山載っている. ただし, shadow-cljsでなければdefault moduleのためには $default という記法が必要.

example fitbit-node

ref. https://www.npmjs.com/package/fitbit-node

これが,

const FitbitApiClient = require("fitbit-node");
const client = new FitbitApiClient({
        clientId: "YOUR_CLIENT_ID",
        clientSecret: "YOUR_CLIENT_SECRET",
        apiVersion: '1.2' // 1.2 is the default
});

こう!

(ns fitbit
  (:require ["fitbit-node$default" :as FitbitApiClient])
(def client
  (FitbitApiClient. #js {:clientId     "YOUR_CLIENT_ID"
                         :clientSecret "YOUR_CLIENT_SECRET"
                         :apiVersion   "1.2"}))

Cannot infer target type in expression

cljsの関数をファイル分割してライブラリとしてrequireしようとすると, js interopのところでエラーする.

型ヒント(^js)をつけると解決.

(ns my-project.core
  (:require [some.fooLib]))
 
(defn wrap-baz [^js/Foo.Bar x]
  (.baz x))

もしくはコンパイルオプションでwarningを抑止.

(set! *warn-on-infer* true)

JavaScritp Likeなオブジェクトを作成するには?

状態を保持するオブジェクトを作成する.

Record方式

Recordの実体はMapなので, assocで情報を更新する. この場合, immutableなので, 更新元のデータはそのうちガベージコレクションされる.

deftype/set!をつかう

deftypeはコンパイルされるとJS Objectになる.

📝deftype(clojure)

reify方式

📝reify(clojure)とatomを利用する.

(defprotocol PersonProtocol
  (get-name [this])
  (set-name [this new-name]))
 
(defn create-person [initial-name]
  (let [name (atom initial-name)]
    (reify
      PersonProtocol
      (get-name [_] @name)
      (set-name [_ new-name] (reset! name new-name)))))

extends, implementsともにclojuescriptは未サポート.

🆚CLJS/Nodejs Interop

nodejsのライブラリをつかう

cljs.nodejsのrequireをつかって, 使いたいライブラリを読み込む必要がある.

(require '[cljs.nodejs :as node])
 
(def Buffer (.-Buffer (node/require "buffer")))
 
(.from Buffer s "utf8")

🆚CLJS/JS Promise(async/await) Interop

JavaScriptの非同期処理との互換性. 📝Promise(JS)をどうするか?

公式ガイド: https://clojurescript.org/guides/promise-interop

async/awaitスタイル interop

  • then/catchをつかう. threading macroできれいにかける.
  • 🔧funcool/promesaをつかう.
  • shadow-cljs/js-awaitをつかう(exeprimental).

このスタイルだとjsスタイルのaysnc/awaitとの親和性は高い. ただ, 複雑な並行処理を書こうとしたときにnative文法のthenだけだとかけないのでpromesaをつかうことになる.

core.asyncスタイルinterop

go/<p!をつかう.

  • p->c: JS Promiseをchannelに変換する.
  • <p!: JS Promiseをchannelに変換した上で(p->c), チャネルから値を読みとる.

このマクロをつかうと, promiseはchannelに変換した上でcljsのcore.asyncのパラダイムで処理できる(ie. async/awaitのパラダイムで処理しない).

(:require
   [cljs.core.async :refer [go]]
   [cljs.core.async.interop :refer-macros [<p!]])

  • 2020にcore.asyncに沿った書き方が提案された(experimental).
  • 2020以前の記事は注意が必要. これからの標準になる?

with REPL

非同期処理とREPLの相性はいまいち. reset!(atom)とかdefをつかってbindingが必要.

CLJS非同期処理とREPL駆動開発を連携させるにはREPLのnsにbindingが必要

JSのAPIコールは基本的には非同期な処理を前提としていて返り値はPromiseを処理する. なので取得結果をREPLにbindingしてゴニョゴニョするためにはREPLのnsにbindingが必要.

(def a (atom nil))
 
(-> (get-balance pubkey)
    (.then (fn [ret] (reset! a ret))))
 
(go (let [ret (<p! (get-balance pubkey))]
      (reset! a ret)))
  • はじめはatomをつかってたけどREPL駆動ならばdefでもいいかも.
  • cljsのcore.asyncにはそもそも同期的にデータを取り出すような >!!や<!!が実装されていない.
  • clojure.core/promiseはcljsには存在しない.

📝Clojure REPL駆動開発

nodejs環境での非同期処理の標準出力が消える(not prn, but use js/console.log)

原因不明だが, nodejs環境でasync処理を書くと出力がでない. ただし処理は完了しているようにみえる. たとえばatomに処理結果をbindすると成功している. これがnodejsの問題なのかshadow-cljsなのか, REPLの問題なのかはわからない. しかし処理は動いているようだ.

Some async output does not appear when using node repl in Calva with shadow-cljs · Issue #1468 · BetterThanTomorrow/calva · GitHub

ローカルのatomに保持するスニペット

(let [s (atom {})]
  (defn t
    ([kw] (get @s kw))
    ([p kw] (.then p (fn [r] (swap! s assoc kw r) r)))))
 
(-> (js/fetch "https://jsonip.com/")
    (t :jsonip))

Clojure/ClojureScript Interop

Clojure x ClojureScriptの共存方法のノウハウ.

  • Clojure/Scriptというように表記されることが多い.
  • ファイル拡張子cljc.

逆引きhowto

sleep処理をしたい

clojure.core.asyncの <!/timeoutをつかうといい.

(require '[clojure.core.async :as a])
 
(a/go
  (doseq [x coll]
    (<! (a/timeout 2000))
    (something!)))

🔗References