Skip to content

Instantly share code, notes, and snippets.

@jiyinyiyong jiyinyiyong/cljs-slide.md
Last active Jul 19, 2018

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
You can’t perform that action at this time.