🔖ClojureScriptの基礎文法まとめ.
ClojureScript文字列操作
フォーマット文字列
goog.string/formatをつかう. clojure.core/formatは同一機能だからか削除されてる.
(goog.string/format "名前: %s, 年齢: %d" "太郎" 30)
ClojureScript例外
- cljs.repl/pst, *e.
- Handling Exceptions and Errors | Lesson 24 | Learn ClojureScript
: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になる.
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には存在しない.
nodejs環境での非同期処理の標準出力が消える(not prn, but use js/console.log)
原因不明だが, nodejs環境でasync処理を書くと出力がでない. ただし処理は完了しているようにみえる. たとえばatomに処理結果をbindすると成功している. これがnodejsの問題なのかshadow-cljsなのか, REPLの問題なのかはわからない. しかし処理は動いているようだ.
ローカルの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
- CLJS API
- ClojureScript Cheatsheet
- Interacting With JavaScript Data | Lesson 13 | Learn ClojureScript
- ClojureScript and JavaScript Interoperability: A Comprehensive Guide