Skip to content

Instantly share code, notes, and snippets.

@niquola
Created November 8, 2022 14:07
Show Gist options
  • Save niquola/62aea96964d41db4d0bfad308fdd9df6 to your computer and use it in GitHub Desktop.
Save niquola/62aea96964d41db4d0bfad308fdd9df6 to your computer and use it in GitHub Desktop.

Код, данные, модели и Сlojure

Философский этюд

Jocker 2022

Николай Рыжиков @niquola

CTO at Health Samurai @niquola - github, telegram, twitter

  • HL7 FHIR
  • Clojure
  • Postgres
  • DevOps

Coming Out

Coming Out

Не люблю доклады! Люблю общаться! Спрашивайте походу!

Основная польза от конференций в разговорах с инетересными людьми в кулуарах!

Почему не вся конференция не кулуары? Выступайте - попадете в кулуары!

Background

  • Academy - PHD in radio-pharmacy
  • Late in IT at 25y
  • Теория? :(
  • Как правильно программировать?

Рефлексия последних 10 лет

Agenda

Гипотеза лингвистической относительности: Язык определяет мышление!

My hx:

  • PHP, VB ! fun
  • Java/C# ! hard
  • SQL ! declarative
  • Ruby ! dsls
  • Clojure ! data & fp

Чему меня научила clojure? Может будет полезно и вам!

2+2 programm

two_plus_two() {
    return 2 + 2;
}

three_plus_three () {}
four_plus_four () {}

// parameterize, generalize!
dobule(x) { return x + x; }
// even more general
op(op, x) { return x op x; }

Patient CRUD

class Patient {
    private string id;
    private string name;
    private Date birthDate;
    //other fields
}

save_patient(Patient pt){
   validate(pt); 
   String cmd = "INSERT INTO patient(name,birthdate) VALUES(?,?) returning *";
   PreparedStatement stmt = conn.prepareStatement(cmd, ...)
   stmt.setString(1, pt.getName());
   stmt.setString(2, pt.getBirthDate());
   ResultSet rs = stmt.executeQuery();
   // get generated id
   pt.setId(rs.getString(0));
   return pt;
}

Practitioner CRUD

class Practitioner { ... }

save_practitioner(Practitioner pt){
    // SAME S....
    // BOILERPLATE?
}

How to abstract, generalize!?

  • Reflection?
  • Annotations?
  • So complicated! WTF?

What if not class/object, but hash-map?

OOP was invented because hash-map was late!

{:type "Patient"
 :name "Nikilai"
 :birth_date "1980-03-05"}

(assoc map :key "value")
(get map :key)
(dissoc map :key)
(for [[k v] map] ...)

Generalize it!

  • keys -> column names
  • type attribute -> table name
  • concat insert command
  • populate values
  • execute

Generalize it!

(defn save [conn res]
  (let [tbl (:type res)]
    (loop [cols [] vals [] params []
           [[k v] & kvs] res]
      (if (nil? k)
        (let [st (jdbc/prepared conn (str "insert into " tbl " (" cols ") VALUES (" vals ")"))
              res (.executeQuery st)]
          (assoc res :id ()))
       (recur (conj cols (nake k))
             (conj vals "?")
             (conj params v)
             kvs)))))

Generalize it!

(save conn
      {:type "Patient"
       :name "Nikilai"
       :birth_date "1980-03-05"})

(save {:type "Practitioner"
       :name "Nikilai"
       :birth_date "1980-03-05"})

one data structure & 100 functions

> Is it better to have 100 functions operate on one data structure > than 10 functions on 10 data structures?

Alan Perlis’ Epigrams on Programming (1982)

Write more generic code!

Clojure essence

  • Program with data & functions!
  • 100 functions

How to deal with deps?

class Repo {
    private Logger logger;
    private Connection conn;
    //other fields
    String doTheJob() { ... }
}

Repo r = new Repo();
r.setLogger(..)
r.setConnection(..)
// or constructor?
r.doTheJob(..)

Components!?

Accidental complexity

  • lifecycle
  • deps
  • extra design choices (constructor, setters, …)

=> coupling (спутаность) => less reuse

What if i have two databases or loggers?

Pass it as params!

doTheJob (Logger logger, Connection connection) { ... }

Or in clojure!

(defn do-the-job [ctx params] ...)

;; decomplected! less choices
(do-the-job {:connection conn-1 :logger logger-config-1})

(do-the-job {:connection conn-2 :logger logger-config-2})

Была ломка - но прошла

  • easy reuse
  • easy test

System

(def ctx
  {:db db-pool
   :http {:server server}})

(do-the-job {:connection conn-1 :logger logger-config-1} params)

(do-the-job {:connection conn-2 :logger logger-config-2} params)

DRY: One source of truth?

Let’s get back to Patient CRUD

  • create table
  • serialize/deserialize json
  • CRUD methods
  • Search
  • Docs

Repeat our self!

Annotations, reflection?

  • Oh, we can define annotations.
  • Then extract it and generate something!

Ruby DSL style

create_table :patient do |t|
  t.string :name
  t.date :birthdate
  t.reference :organization
  t.timestamps
end

class Patient < ApplicationRecord
  belongs_to :organization
end

It’s just a data, meta-data!

Why not just encode it as data

models
{Patient
 {:fields {:name      {:type string :required true}
           :birthDate {:type dateTiem}}}

 Practitioner
 {:fields {:name      {:type string :required true}
           :birthDate {:type dateTiem}}}}

What we can do with meta-data?

  • generate tables, classes, docs
  • use as params in generic functions
    • validation
    • persistence
    • etc
  • pass over the network!
  • what ever interpretation!

How to encode data?

  • json
  • yaml
  • edn!

EDN in 2 minutes

;;primitives
1 100.0 "string" true

;; special strings
:keyword ;; special string to be the key in a map
symbol ;; sepcial string to name the functions 

;; map
{:key "value"
 :vector [1 2 3]
 :set #{}
 :symbol Patient
 :date #inst"2022-01-01"}

Инь и Янь: Code & Data

This is magic that you can extract part of algorithm into data structure.

Clojure Data DSLs

Everything can be expressed as a data!

  • http server routing
  • access policies
  • transformations
  • workflows

Just few examples: Routing

(def routes
  {:GET    'root
   "files" {:path* {:GET 'file}}
   "users" {:GET  'list
            :POST 'create
            [:uid] {:GET 'show
                    :PUT 'udpate
                    :DELETE 'destroy}}})

Just few examples: SQL

(assoc 
 {:select :*
  :from :Patient
  :where [:= :id "pt-1"]}
 :limit 100)

Even code (homoiconicity)

(plus 1 1) => [“plus”, 1, 1]

#macro system

data -> f -> data

If everything is data!

You program is just a set of functions transforming data!

  • parse http request into map
  • find route in a map
  • build sql as map
  • execute
  • transform
  • build http request string

Model-driven system

system = meta-data (models) + generic interpreters

Model refers model

To refer one model from another we need to **names**! Use symbol for names!

{ns myapp
 import #{lib}

 patient
 {:zen/tags #{zen/schema}
  :keys {}}

 create-pt
 {:zen/tags #{lib/http-op}
  :schema patient
  :route [:GET "/patient"]}}

How model driven system will look?

~/Downloads/zen-system.jpeg

How model driven system will look?

  • Introspection
  • Configurable
  • Extensible
  • Reusable
  • Define your Models/DSLs
  • Compose DSLs with refs
  • Create model modules (aka libs)
  • Reuse generic LSP

System evolution

  • Hard-coded (specific use case)
  • Customizable (group of use cases)
  • Platform (domain of use cases)

Allow the user to program your system at the end!

Reflect! Thx

@niquola tg, tw, gh

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