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;
}
既然刚刚在 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");
偶然在小红书上刷到一个前端面试题,顺手做了一下。
要求:实现一个带并发限制的异步调度器 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"));