Notes

在 Haskell 中写一个简单的 SQL DSL

很喜欢 Rails 的 Active Record 风格的查询接口,比如这个

result = viewable_by(user, order: order, preload: preload)
result = by_status(result, status)
result = result.where(id: ids) if ids
result = result.where("reviewables.type = ?", Reviewable.sti_class_for(type).sti_name) if type
result = result.where("reviewables.category_id = ?", category_id) if category_id
result = result.where("reviewables.topic_id = ?", topic_id) if topic_id
result = result.where("reviewables.created_at >= ?", from_date) if from_date
result = result.where("reviewables.created_at <= ?", to_date) if to_date

很好的运用了重绑定的做法,允许根据上下文动态调整查询条件。

在 Haskell 中怎么写呢?尝试了一下。

{-# LANGUAGE NoImplicitPrelude #-}

import Relude

data SqlBuilder = SqlBuilder
  { selects :: [String],
    from :: String,
    joins :: [String],
    wheres :: [String]
  }
  deriving (Show)

prettySql :: SqlBuilder -> String
prettySql builder =
  "SELECT "
    ++ intercalate ", " (selects builder)
    ++ "\n  FROM "
    ++ from builder
    ++ ( if null (joins builder)
           then ""
           else
             "\n  JOIN "
               ++ intercalate "\n  JOIN " (joins builder)
       )
    ++ ( if null . wheres $ builder
           then ""
           else
             "\n WHERE " ++ intercalate "\n       AND " (reverse . wheres $ builder)
       )

where_ :: String -> SqlBuilder -> SqlBuilder
where_ condition builder = builder {wheres = condition : wheres builder}

join_ :: String -> SqlBuilder -> SqlBuilder
join_ condition builder = builder {joins = condition : joins builder}

selectFrom :: [String] -> String -> SqlBuilder
selectFrom columns from =
  SqlBuilder
    { selects = columns,
      from = from,
      joins = [],
      wheres = []
    }

might :: (a -> SqlBuilder -> SqlBuilder) -> Maybe a -> SqlBuilder -> SqlBuilder
might _ Nothing builder = builder
might f (Just x) builder = f x builder

list :: (Show a) => [a] -> String
list items = "(" ++ intercalate ", " (map show items) ++ ")"

main = do
  putStrLn $ prettySql query
  where
    ids = Just [1, 2, 3]
    typ = Just "miao"
    category_id = Nothing :: Maybe Int
    topic_id = Just 1234
    from_date = Just "2023-01-01"
    to_date = Nothing :: Maybe String

    query =
      selectFrom ["name", "age", "id"] "users"
        & join_ "profiles ON users.id = profiles.user_id"
        & where_ "age > 18"
        & might where_ (("id IN " ++) . list <$> ids)
        & might where_ (("type = " ++) . show <$> typ)
        & might where_ (("category_id = " ++) . show <$> category_id)
        & might where_ (("good_topic_id = " ++) . show <$> topic_id)
        & might where_ (("created_at >= " ++) . show <$> from_date)
        & might where_ (("created_at <= " ++) . show <$> to_date)

输出结果:

SELECT name, age, id
  FROM users
  JOIN profiles ON users.id = profiles.user_id
 WHERE age > 18
       AND id IN (1, 2, 3)
       AND type = "miao"
       AND good_topic_id = 1234
       AND created_at >= "2023-01-01"

(当然,没考虑安全性,这只是一个为了测试 haskell 能不能写出类似 ruby 那样优雅的查询代码的例子)

关于 Ruby 有多少解构赋值 / pattern matching 这件事。

  • 基础

    a, b = [1, 2]
    # a = 1 ; b = 2
  • 复杂的基础

    a, (b, c), d = [1, [2, 3], 4]
    # a = 1 ; b = 2 ; c = 3 ; d = 4
  • 解构数组

    a, *b = [1, 2, 3, 4]
    # a = 1 ; b = [2, 3, 4]
  • in 关键字

    [1, 2, 3] in a, b, c
    # a = 1 ; b = 2 ; c = 3
  • =>

    [1, 2, 3] => a, b, c
    # a = 1 ; b = 2 ; c = 3

    似乎只有右边放变量才能解构哈希

    {a: 1, b: 2} => a:, b:
    # a = 1 ; b = 2
  • 更奇怪的解构

    通过类型来解构

    [1,2,3,"str",4,5,6] => *a, String => b, *c
    # a = [1, 2, 3] ; b = "str" ; c = [4, 5, 6]

    通过值来解构

    [1,2,3,"str",4,5,6] => *a, 4 => b, *c
    # a = [1, 2, 3, "str"] ; b = 4 ; c = [5, 6]

    但是类型不能是值……?

    [1,2,3,String,4,5,6] => *a, String => b, *c
    #  [1, 2, 3, String, 4, 5, 6] does not match to find pattern (NoMatchingPatternError)
  • 还能类似 Elixir 那样 pin 一个值

    b = 3
    [1, 2, 3, 4, 5] => *a, ^b, *c
    # a = [1, 2] ; b = 3 ; c = [4, 5]
  • 也能自定义解构规则

    class Test
        def deconstruct_keys(keys)
            p keys
            keys.each_with_object({}) do |key, hash|
            hash[key] = rand
            end
        end
        def deconstruct
            [1,1,4,5,1,4]
        end
    end
    
    t = Test.new
    t => a, b, c, d, e, f
    # a = 1 ; b = 1 ; c = 4 ; d = 5 ; e = 1 ; f = 4
    t => a:, b:, c:
    # a, b, c 为随机数

这是一段中文测试句子。

This is a test sentence of english.

これは日本語のテスト文です。

aewhf laewh flawheflkawhelfk awe

在 C++ 中模拟一个管道运算符。

#include <iostream>
template <typename T> class Pipe {
private:
public:
  T value;
  Pipe(T value) : value(value) {}

  template <typename F>
  auto operator>>(this Pipe<T> &&self, F &&func)
      -> Pipe<decltype(func(std::declval<T>()))> {
    using ReturnType = decltype(func(std::declval<T>()));
    return Pipe<ReturnType>(func(std::move(self.value)));
  }

  template <typename F>
  auto to(this Pipe<T> &&self, F &&func)
      -> Pipe<decltype(func(std::declval<T>()))> {
    using ReturnType = decltype(func(std::declval<T>()));
    return Pipe<ReturnType>(func(std::move(self.value)));
  }
};

int main() {
  auto result = Pipe<int>(5) >> ([](int &&x) { return x * 2; }) >>
                ([](auto &&x) { return x + 1; });
  std::cout << result.value; // Should return 11
  return 0;
}
TypeScript

既然刚刚在 Notes 里写了个面试题,忍不住怀念一下自己的第一次面试(which 挂了)。

当时没写出来(确实菜),虽然场下不紧张了马上就写出来了。

题目: 实现一个 LazyMan

  • LazyMan('Hank') 输出:

    Hi! This is Hank!
  • LazyMan('Hank').sleep(10).eat('dinner') 输出

    Hi! This is Hank!
    // 等待 10 秒..
    Wake up after 10
    Eat dinner~
  • LazyMan('Hank').eat('dinner').eat('supper') 输出

    Hi This is Hank!
    Eat dinner~
    Eat supper~
  • LazyMan('Hank').sleepFirst(5).eat('supper') 输出

    // 等待 5 秒
    Wake up after 5
    Hi This is Hank!
    Eat supper~
题解

现在无比丝滑的就做出来了,反而很难想象当时为什么没写出来。

function LazyMan(name: string) {
  const jobs: (
    | { type: "sleep"; time: number }
    | { type: "eat"; what: string }
    | { type: "say"; what: string }
  )[] = [
    {
      type: "say",
      what: `Hi! This is ${name}!`,
    },
  ];

  function sleepFirst(this: ReturnType<typeof LazyMan>, time: number) {
    jobs.unshift({
      type: "sleep",
      time,
    });
    return this;
  }

  function eat(this: ReturnType<typeof LazyMan>, what: string) {
    jobs.push({
      type: "eat",
      what,
    });
    return this;
  }

  function sleep(this: ReturnType<typeof LazyMan>, time: number) {
    jobs.push({
      type: "sleep",
      time,
    });
    return this;
  }

  setTimeout(async () => {
    for (const job of jobs) {
      switch (job.type) {
        case "say":
          console.log(job.what);
          break;
        case "sleep":
          await new Promise((r) => setTimeout(r, job.time * 1000));
          console.log(`Wake up after ${job.time}`);
          break;
        case "eat":
          console.log(`Eat ${job.what}~`);
          break;
      }
    }
  }, 0);

  return {
    sleep,
    sleepFirst,
    eat,
  };
}

// LazyMan('Hank');
// LazyMan('Hank').sleep(10).eat('dinner');
// LazyMan('Hank').eat('dinner').eat('supper');
// LazyMan("Hank").sleepFirst(5).eat("supper");
TypeScript

偶然在小红书上刷到一个前端面试题,顺手做了一下。

要求:实现一个带并发限制的异步调度器 Scheduler,保证同时运行的异步任务最多 MAX_LENGTH 个,使得以下程序能正确输出

const timeout = (time) =>
  new Promise((resolve) => {
    setTimeout(resolve, time);
  });
const MAX_LENGTH = 2;
const scheduler = new Scheduler(MAX_LENGTH);
const addTask = (time, order) => {
  scheduler
    .add(() => {
      return timeout(time);
    })
    .then(() => console.log(order));
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");

//  output: 2 3 1 4
// 一开始,1、2两个任务进入队列
// 500ms时,2完成,输出2,任务3进队
// 800ms时,3完成,输出3,任务4进队
// 1000ms时,1完成,输出1
// 1200ms时,4完成,输出4

感觉还是有点简单,五分钟内肯定是能做出来了。

题解

一开始漏了异常处理的情况,让 gpt 看了眼指了出来

class Scheduler {
  running = 0;
  pending_tasks: (() => void)[] = [];
  queue_len: number;

  constructor(queue_len: number) {
    this.queue_len = queue_len;
  }

  async add<T>(promiseCb: () => Promise<T>): Promise<T> {
    if (this.running >= this.queue_len) {
      await new Promise<void>((resolve) => {
        this.pending_tasks.push(resolve);
      });
    }
    this.running++;
    try {
      return await promiseCb();
    } catch (err) {
      throw err;
    } finally {
      const resolve = this.pending_tasks.shift();
      if (resolve) {
        resolve();
      }
      this.running--;
    }
  }
}

const scheduler = new Scheduler(2);

const sleepAndLog = (time: number, ...txt: unknown[]) =>
  new Promise((r) => setTimeout(r, time)).then(() => console.log(...txt));
const sleepAndRaiseLog = (time: number, ...txt: unknown[]) =>
  new Promise((r) => setTimeout(r, time)).then(() => Promise.reject(...txt));

scheduler.add(() => sleepAndLog(10000, "1"));
scheduler.add(() => sleepAndRaiseLog(5000, "2"));
scheduler.add(() => sleepAndLog(3000, "3"));
scheduler.add(() => sleepAndLog(4000, "4"));