Instantly share code, notes, and snippets.

Embed
What would you like to do?
Slides can be rendered with http://repo.memkits.org/sedum-slide/

函数式语言 ClojureScript 在前端开发的体验


Me

题叶, ChenYong, 上海

Teambition, 饿了么, 积梦智能(工业大数据).

Single Page Applications.

CoffeeScript, Backbone, React, TypeScript.

ClojureScript since 2016~


Clojure, data manipulation

(->> data
     (partition-by (comp boolean :high))
     (partition 2 1)
     (mapcat (fn [[lbounds rbounds]]
               (let [left-bound (last lbounds)
                     left-val (hi-or-lo left-bound)]
                 (map #(let [right-val (hi-or-lo %)
                             diff (Math/abs (- right-val left-val))]
                         {:extremes [left-bound %]
                          :price-range diff
                          :midpoint (+ (min right-val left-val)
                                       (/ diff 2))})
                      rbounds))))
     (clojure.pprint/pprint))

ClojureScript, Reagent

(ns example
  (:require [reagent.core :as r]))

(defn simple-component []
  [:div
   [:p "I am a component!"]
   [:p.someclass
    "I have " [:strong "bold"]
    [:span {:style {:color "red"}} " and red "]
    "text."]])

(defn render-simple []
  (r/render [simple-component]
    (.-body js/document)))

小调查

  • 多少人熟悉 React?
  • 多少人熟悉 Lisp?

First impressions

大学从 CoffeeScript 接触到 Haskell, 了解到函数式语言.

从 React 函数式特性, 开始深入了解 ClojureScript.

Clojure 在 2007 年发布, 编译到 JVM. ClojureScript 2011 年发布, 编译到 JavaScript.

ClojureScript 生态在 2015 逐渐成熟, 但是工具链到 2017 年开始成熟, 主要是对 npm 生态的支持.


Functional Programming

Higher-order functions, lodash, clojure.core unitilies.

Pure functions, isolation of states.

Immutability.

Everything is an expression, DSL.

Lazy evaluation, Algebra data types(No goal...)


ClojureScript Features

编译到宿主平台运行, Clojure->JVM, ClojureScript->JavaScript

Clojure, Lisp, Macro. Functional Programming, Immutability.

Dynamic typed. No algebra data types.

Data-oriented programming, rich data structures on Lisp, more built-in functions.

Simplicity. (Simple made easy.)

http://clojure-script.org


Where to use ClojureScript?

  • JavaScript platforms: browsers, Node.js , React Native.

  • Scripting, data manipulation. Data-oriented language. EDN(Extensive Data Nodation), like JSON but richer.

  • Creating Virtual DOM, states management.


My tiny apps in ClojureScript

Respo: virtual DOM library, Cirru Calcit Editor

Tiny app: Todolist, Diary, 图标选择工具, 便签.

...more tools for dev.


ClojureScript: syntax and toolchains


Lumo: a REPL

命令行工具. 基于 V8 以及 Node.js , 适合写脚本, 或者使用 REPL:

$ npm install -g lumo-cljs
$ lumo
Lumo 1.8.0
ClojureScript 1.9.946
Node.js v9.2.0

cljs.user=> (println (+ 1 2 3))
6
nil
cljs.user=>

Basics

(+ 1 1) ; => 2
(not true) ; => false
(+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2

(if false "a" "b") ; => "b"

(map inc [1 2 3]) ; => (2 3 4)
(reduce + [1 2 3 4])
; = (+ (+ (+ 1 2) 3) 4)
; => 10

Basics

(def x 1)

; The [] is the list of arguments for the function.
(defn hello [name]
  (str "Hello " name))
(hello "Steve") ; => "Hello Steve"

(fn [] "Hello World") ; => fn

(def stringmap {"a" 1, "b" 2, "c" 3})
(class #{1 2 3}) ; => clojure.lang.PersistentHashSet

Difference with pure functional languages

Clojure 并不是纯函数的编程语言. 没有类型系统, 只有基础的静态检查.

还是会有副作用, React 的抽象能力很强大, 那些并不是通过函数式编程处理的.

纯函数语言会有更强大的工具, 学习成本也更高.


Difference with JavaScript

  • Lisp syntax, unfamiliar
  • tail recursions, no for/while
  • Immutabilities, use atom for states
  • Static analysis(like TypeScript)
  • Different compilers and communities
  • Packages may be on Clojars or npm

ClojureScript Compiler

ClojureScript compiler in JVM / self-hosted ClojureScript(in JavaScript)

based on Google Closure Compiler, 不是社区主流

goog.module(‘foo’);

var Quux = goog.require(‘baz.Quux’);

exports.Bar = function() { /**/ };

Toolchains

Before 2017: ClojureScript compiler, Leiningen, Boot, Figwheel, (类比 Browserify, Grunt, Gulp, Webpack)

Command line tools: Lumo/Planck(类比 CoffeeScript)

开发/打包工具 shadow-cljs(类比 Parcel)

Plenty of libraries from Maven, Clojars & npm.


Why ClojureScript


React Concepts

  • Pure rendering, Stateless Components
  • shouldComponentUpdate, Immutable data
  • Higher-order components
  • Composition instead of inheritance

...从 Functional Programming 借鉴的特性.


JavaScript Problems

  • No immutabable data(Persistent data structure)
  • No type checking
  • Less pure in functions
  • Babel(instead of Macros)

Need more features from Functional Proramming. (Facebook ReasonML...)


React side-effects

有函数式编程, 也有面向对象的妥协, 以及维护大量局部状态.

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

ClojureScript version

functional stateless components.

(defn simple-component []
  [:div
   [:p "I am a component!"]
   [:p.someclass
    "I have " [:strong "bold"]
    [:span {:style {:color "red"}} " and red "] "text."]])
(defn three-canvas [attributes camera scene tick]
  (let [requested-animation (atom nil)]
    (reagent/create-class
      {:display-name "three-canvas"
       :reagent-render
       (fn three-canvas-render []
         [:canvas attributes])
       :component-did-mount
       (fn three-canvas-did-mount [this]
         (let [e (reagent/dom-node this)
               r (create-renderer e)]
           ((fn animate []
              (tick)
              (.render r scene camera)
              (reset! requested-animation (js/window.requestAnimationFrame animate))))))
       :component-will-unmount
       (fn [this]
         (js/window.cancelAnimationFrame @requested-animation))})))

immutable-js code is tedious

// ES6
const {profile, image, age, gender} = this.props.client;
// ImmutableJS
const profile = Immutable.get(this.props.client, 'profile');
const image = Immutable.get(this.props.client, 'image');
const age = Immutable.get(this.props.client, 'age');
const gender = Immutable.get(this.props.client, 'gender');

Maybe Immer today?


ClojureScript data

Immutable by default.

(conj [1 2] 3) ;; [1 2 3]
(conj {:a 1 :b 2 :c 3} [:d 4]) ;; {:d 4, :a 1, :c 3, :b 2}

(get {:a 1 :b 2 :c 3} :b) ;; 2
(get [10 15 20 25] 2) ;; 20

(assoc {:a 1} :b 2) ;; {:b 2, :a 1}
(assoc {:a 1 :b 45 :c 3} :b 2) ;; {:a 1, :c 3, :b 2}

(dissoc {:a 1 :b 2 :c 3} :b) ;; {:a 1, :c 3}

(dissoc {:a 1 :b 14 :c 390 :d 75 :e 2 :f 51} :b :c :e)
;; {:a 1, :f 51, :d 75}

Conditional rendering in JSX

if/switch statements:

render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      {isLoggedIn ? (
        <LogoutButton onClick={this.handleLogoutClick} />
      ) : (
        <LoginButton onClick={this.handleLoginClick} />
      )}
    </div>
  );
}

ClojureScript solution

(defn calc-bmi []
  (let [{:keys [height weight bmi] :as data} @bmi-data
        h (/ height 100)]
    (if (nil? bmi)
      (assoc data :bmi (/ weight (* h h)))
      (assoc data :weight (* bmi h h)))))

Redux

类库提供的状态管理方案.

import { createStore } from 'redux'
const store = createStore(todos, ['Use Redux'])
​
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
​
store.dispatch(addTodo('Read the docs'))
store.dispatch(addTodo('Read about the middleware'))

ClojureScript Atoms and swap!

States can be changed:

; Here's a simple counter using an atom
(def atom-int (atom 1))

(swap! atom-int inc)   ; @x is now 2
(reset! atom-int 100)  ; @x is now 100
(swap! atom-int inc)   ; @x is now 101

Mutate states by references.

Used in memoization.


ClojureScript Experiences


Dynamic typing

动态的数据. nil problems.

(get {:a 1} :b) ; nil

Trade-offs: 开发轻松而且灵活, 然而有些动态类型的问题难以调试. 对比 ReasonML.

社区新的尝试: Clojure Spec 运行时的数据检查.


Macros

Macro 的语法, 带来的便利.

pipe operator 在 ClojureScript 当中很随意就使用. 没有类型, 用错有点麻烦.

user=> (->> (range)
            (map #(* % %))
            (filter even?)
            (take 10)
            (reduce +))
1140

;; This expands to:
user=> (reduce +
               (take 10
                     (filter even?
                             (map #(* % %)
                                  (range)))))
1140

Tail recursions

Clojure 没有 for/white 那种写法, 要用尾递归表达循环:

(defn factorial [n]
    (loop [cnt n
           acc 1]
      (if (< cnt 2) acc
        (recur (dec cnt) (* cnt acc)))))

需要很多时间来转变思维方式.


Immutable data

Value is immutable. Information does not change.

以往面临对内存开销问题, 在实现上, 使用结构复用(Persistent Data Structure, structural sharing), 减少内存的开销以提升性能.

vector


Interop

(def text js/globalName) ;; JS output: namespace.text = globalName;

(def t1 (js/MyType.)) ;; JS output: namespace.t1 = new MyType;

(.hello js/window) ;; JS output: window.hello();

(def my-type (js/MyType.))  ;; JS output: namespace.my_type = new MyType;
(def name (.-name my-type)) ;; JS output: namespace.name = namespace.my_type.name;

(set! (.-name my-type) "Andy") ;; JS output: namespace.my_type.name = "Andy";

await/async proposal


npm 模块

以往对 npm 模块支持不佳. 到 shadow-cljs 情况改善了. 现在可以引用 CommonJS 以及 ES6 语法的模块.

(ns app.main
  (:require ["dayjs"   :as dayjs]
            ["shortid" :as shortid]
            ["lodash"  :as lodash]
            ["lodash"  :refer [isString]]))

可以很方便引入 React 和 npm 上的各种类库.


Debugging ClojureScript


shadow-cljs: a code compiler and bundler

http://shadow-cljs.org/

  • filename hashing, async loading
  • hot code swapping, static analysis(type warnings)
  • auto-generate shim files(for Google Closure Compiler)
  • npm friendly(use npm packages), and:
npm install -g shadow-cljs

shadow-cljs.edn

{:source-paths ["src"]
 :dependencies [[fipp "0.6.12"]]
 :builds {:app {:output-dir "target/"
                :asset-path "."
                :target :browser
                :modules {:main {:entries [app.main]}}
                :devtools {:after-load app.main/reload!
                           :http-root "target"
                           :http-port 8080}}}}

Think it as a "Webpack for ClojureScript"...


I’m calling it: ClojureScript now has the best Dev XP of any compile-to-js language

@pesterhazy

shadow-cljs browser demo.


Hot code swapping

区分了数据和状态(atom), 一定的纯函数的约束.

热替换更加轻松和可靠.

defonce 来存储全局状态.

(defonce reflexes (atom #{}))

(defn register-reflex [name]
  (swap! reflexes conj (resolve name)))

Static analysis & warnings

=----- WARNING #1 --------------------------------------------------------------
 File: /Users/chen/repo/topixim/tabletwo/src/app/comp/previewer.cljs:107:41
=-------------------------------------------------------------------------------
 104 |                     "\n"
 105 |                     "\n"
 106 |                     (->> (:paragraphs article)
 107 |                          (sort-by first typo)
=----------------------------------------------^--------------------------------
 Use of undeclared Var app.comp.previewer/typo
=-------------------------------------------------------------------------------
 108 |                          (map #(:content (last %)))
 109 |                          (string/join (str "\n" "\n" "----" "\n" "\n"))))
 110 |            html (str "<pre>" (escape-html content) "</pre>")]
 111 |        (.. child -document (write html))))}
=-------------------------------------------------------------------------------

调试工具

相对于其他语言, ClojureScript 生成的代码比较复杂. 加上有 macro, lazy evaluation 的影响, 有可能更难调试.

SourceMap, cljs-devtools 提供更好的代码和数据展示.

DEMO


Remote REPL

远程连接的运行环境的 REPL, 调试代码

DEMO


括号的问题

开发 Lisp 是必须使用工具管理括号嵌套(比如 EMACS...)

而且用了工具以后是可以更加高效的. 我用 calcit-editor...

DEMO


结尾...


Try ClojureScript

Lisp 带来的高阶函数和 Macro 让语言更有表达的能力.

Clojure 语言设计相比 JavaScript 有更多时间思考和打磨.

Immutability 带来深入的对于数据和状态的思考.

跳出 JavaScript 和面向对象来理解程序.(Functional Programming)

推荐 shadow-cljs Reagent 的方案开发页面.


Communities


希望更多的人能用到 Functional Programming 的成果.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment