用 Angular 弄了一个背单词的网站——eliseos.org

Like
Like Love Haha Wow Sad Angry
18211

弄了一个背单词的网站——eliseos.org,或者叫 jingtu.io。(这俩名字其实是一个意思。当然这个网站不主要是为了背单词而开发的,不过目前只有背单词的还能用。)


它是 Angular 开发的,同时用了一些时新的技术,比如前端的 redux-observable, graphql, apollo client,后端的 inversify, sequelize-typescript,另外我还自己搞了一些用于自动生成 graphql schema 的 decorator——

@Table
export class Language extends NoPrimaryOrdinaryModel<Language> {

  @Default(DataType.UUIDV1)
  @Column
  @GraphQLFieldToType
  id: string;

  @PrimaryKey
  @Column
  @GraphQLFieldToType
  name: string;

  @GraphQLFieldJsonLike(() => Kv)
  @Column(DataType.JSONB)
  namesInLang: KvInterface;

  @HasMany(() => LanguageOfLanguage, 'languageName')
  languageOfLanguages: LanguageOfLanguage[];
}

 

是不是有点像 Java 了?这么写就会生成一个叫 Language 的表,有这么些 column 和 association,这部分是 sequelize-typescript 的功能,还会生成一个叫 Language 的 graphql schema,这部分是我自己写的。难也不难,主要是提供一种思路。

另外写的过程中我也理解了为什么 sequelize-typescript 的 decorator 参数里,类型是 lazy 的(即以上代码里的 () => LanguageOfLanguage。这是因为如果不 lazy,出现类的循环引用的时候,有一方会变成 undefined。我碰到了这个问题,所以我自己也用了 thunk 形式作为参数。

然后对于 query 也有这么些个 decorator,比如——

@GraphQLQueryField(userGraphqlType, {
  token: {
    type: new GraphQLNonNull(GraphQLString)
  }
}, 'Checks if a token is valid. Returns the validated user.')
async checkToken(parentValue: any, {token}: {
  token: string
}, context: ContextInterface) {
  return await context.services.userService.checkToken(token);
}

 

其实也没什么学问,主要是我看着原来 graphql-js 的 object literal 结构太不稳当了,嵌套一深,随便哪里类型写错了,TypeScript 就抛一大堆错(其实挺稳当的,只要你把 object literal 拆分开来,每一级都标注类型,报错就不会大规模传播)。

用上了 decorator,顺带用了 inversify——一个依赖注入管理器,以及 TypeScript 的一套东西,谁说 Node.js 不可以工程化开发呢?

至于前端,也是从 TypeScript 中获益良多。Angular 原生就是依赖 TS 的,也是原生就依赖 decorator 的,也是原生就依赖注入的。这么看 JS 生态系统前后端的技术选型已经趋同了,我相信这是好的趋势。

还有什么要说的话,就是我把以前答过的Axurez:如何评价 TypeScript 最新加入的 Discriminated union type?这个定义 tagged union 的简便语法,真的给用到了实战里——不仅仅是定义 redux actions,而且我写了一个可以深度 get 和 set 的 Map,因为要用递归指涉自身,还必须用到 tagged union。这个类型如下:

export type RecMap<K, V> = Map<K, { type: 'V', value: V } | { type: 'M', map: RecMap<K, V> }>;

 

整个弄下来的感想就是,Angular 是真的好用,Angular 生态是真的不错,universal 完全按官方走一遍就活了,现在线上运行的版本就是 universal 的,右键查看源码可以看到是渲染好的页面发过来的。angular cli 一路可以 generate 到底,基于 NgModule 的路由懒加载也是开箱即用,不需要任何配置,非常美妙。

还有一点就是,还是纯 css 库比较稳妥,带 JS 的反而不行,我用了 material design components 和 material design lite(因为前者没实现 chip……),还是把 js 拆了用的。一方面是它带的 JS 不一定合你的意,另一方面是它哪怕是用了你用的框架,它的 API 设计也不一定合你意(这也是我不用 Angular Material 的原因)。另外用了 Angular 这样的框架,它封装的那点功能你自己封装也要不了多久。

如果说还有一点感想,那就是我好像有点达成以前的目标了。以前总是感觉 sequelize 要写一套定义,要写一套接口,graphql 还要写一套定义,来来回回同样的东西要写好多遍。现在借助 decorator 真的实现了只写一遍。现在还有 sequelize-auto-migrations 这样的东西,可以自动根据模型的更改生成 migration,简直不要太省劳动力。


说了这么多关于开发的功能,那么这个网站怎么用呢?(推荐桌面使用,虽然移动端也完全可用)

关于背单词功能,如下图,点击右侧的「巴别」(巴别,用 wikipedia 的人应该见过,典出巴别塔,我用来指代和语言相关的模块)中的「学习」,就来到了学习界面(同时在线统计是用 socket.io 实现的,不过用户数量目前并不对,页面是对的。这里有个彩蛋,没网的时候 logo 就会黑掉):

你可以在右侧的「语料」里粘贴进英文文章,然后点击「使用这篇文章」:

它会把文章跟服务器同步,解析出你认识的、不认识的、没决定过的词汇,分别标上白色、红色和绿色:

正如提示所说的

在左边粘贴文章,并点击按钮,来提取文章中的单词。
绿色表示状态未知的单词,划过标记为认识,单击标记为不认识,再单击标记为认识,以此类推。
你可以在概览中回顾不认识的单词。

照着做就行了。我还弄了一个功能就是添加和查看单词释义。把鼠标悬停在不认识的(即红的)单词上,会出现悬浮框:

点击加号就会出现文本框,可以添加释义,以及选择释义的语言(这个网站处于我个人的恶趣味,是支持了四种语言的,用右上角的语言选单可以极速切换语言)。添加完之后,或者本来就已经有人添加过,就会是这个样子:

本来应当是可以代理一个其他网站的 API,或者干脆服务器上搞一个词库的,实际上应该是可行的,但我还是想先试试 UGC 一段时间(估计效果不好)。添加释义的另一个好处就是可以在个人主页上显示……比如我的主页

个人主页怎么进?登陆之后右上角的用户名可以直接点击:

或者鼠标悬停,在下拉菜单里选「个人档」也行。

如果你误操作了,一方面你可以再次点击单词,在白色和红色之间切换,另一方面也可以在右下角看到所有红色、白色词汇的列表,点击就可以复归「未确认」状态,即绿色。

确认单词为未知之后,不仅单词会被同步到服务器,单词所在的句子也会被同步到服务器,在右侧的「巴别—概览」中可以回看:

未知单词在句子中的未知也会标红。(这里本来是给单词加上 html 标签,然后渲染 html 的,但是会有注入的危险,所以最后强行把句子 split 成一个结构体,用 ngFor + ngIf 渲染了)

以上大概就是目前能用的内容。设置的个人档也可以更新

还可以生成邀请链接,推广注册。

如果遇到什么 bug,请一定联系我。

至于为什么要做这个网站?这个网站除了背单词之外,还是干什么的?就如关于页面所说:


Eliseos 就是「极乐净土」的西班牙语形式。我用西班牙语是因为其他语种类似的域名都注册了(elysium, elysian, elysion 之类的)。其实我原来有个 elysion.tech,但是 .tech 太冷门了,很多地方识别不出来。这网站还有个域名,叫「净土」—— jingtu.io

根据维基百科:

至福乐土(Elysium)或是乐土平原(Elysian Fields)(古希腊语:Ἠλύσιον πεδίον,Ēlýsion pedíon,音译:伊利西恩、伊利西昂。中文翻译上亦可名为归静乐土或是归净乐土,意为回归安静或回归纯净的乐土。)是一个来世观的理念,随著时间的推移并由一些希腊的宗教教派的仪式与哲学流派所延续发展。

我最早是在《圣斗士星矢》里接触到这概念的。在这里,我想用它指代这个网站,是因为我想做一个能进行深刻思想交流(包括语言、知识)的地方。

Like
Like Love Haha Wow Sad Angry
18211

什么是 Haskell 中的 GADT(广义代数数据类型)?

Like
Like Love Haha Wow Sad Angry
3912

先看看没有 GADT 的时候我们在做啥。最简单的,比如定义一个列表:

List a = Nil | Cons a (List a)

它是在做啥呢?

  • 先看等号左边,它首先定义了一个叫 List 的 type constructor,它接受一个类型,并返回一个新的类型。其实就是类型的函数。
  • 再看右边,它告诉我们有两种方式获得一个类型为 List a 的值,一个叫 Nil,一个叫 Cons a (List a)。
    • Nil 的话,你直接写 Nil,它就是一个 List a 类型的值,至于这个 a 具体是啥,需要结合上下文推导才知道。
    • 而 Cons a (List a) 呢,它是接受两个参数,一个类型是 a,一个类型是 List a,然后返回一个 List a 类型的值。至于这个 a 是啥,我们同样是需要推导才能知道的,不过是这里的推导比较显著罢了,因为直接就是第一个参数的类型。

那么我们可以用函数形式给这俩表示出来: Continue reading “什么是 Haskell 中的 GADT(广义代数数据类型)?”

Like
Like Love Haha Wow Sad Angry
3912

例说 C 语言类型声明

Like
Like Love Haha Wow Sad Angry
921
C 语言的类型声明,由于某些历史局限性,在某些情况下显得相当复杂。下面让我们来渐渐深入 C 语言类型声明的谜团,一探究竟。

 

指针和数组

一重声明

以下这些问题,我相信即使是最基本的初学者也不会有太大困难:

你会声明数组吗?

int a[5]; // 包含 5 个元素。

你会声明指针吗?

int *a;

二重声明

你会声明双重指针吗?

Continue reading “例说 C 语言类型声明”

Like
Like Love Haha Wow Sad Angry
921

【C++ 模板元编程入门】在编译期实现 Peano 数

Like
Like Love Haha Wow Sad Angry
4

基本知识

类型的函数

我们都知道模板可以接受类型作为「参数」。同样地我们也可以有「返回值」,从而构造类型的函数。基本的范式是:

template<class T>
struct Computed {
  using type = T;
}

这就构造了一个名为 Computed 的,接收一个类型参数,返回这个类型本身的函数,用法如 Computed<float>::type,这个类型应当还是 `float`。
为什么要包一层 struct?这是因为 C++ 不支持对 using 的特化。这样的代码是不行的:

template <class T>
using computed<T> = T;

template<>
using computed<int> = double;

至于为什么不支持,我没有了解。

特化

什么是特化?你可以理解为模式匹配,就像 Haskell 中的写法一样。

template<class T>
struct Computed {
  using type = T;
}

template<>
struct Computed<int> {
  using type = double;
}

这样当你调用 Computed<bool>::type 时,得到的结果是 bool,而调用Computed<int>::type得到的结果却是double。当然这种匹配是遵循一定规则的,比如更「具体」的特化优先匹配,这跟 Haskell 谁在前谁先试着匹配不太一样。在 Haskell 中就好比:

data Type = Bool | Double | Int

computed :: Type -> Type
computed Int = Double
computed t = t

实际上有了这层对应,如果你知道怎么在 Haskell 中实现 Peano 数,那么 C++ 中的实现基本就是无脑翻译了。如果你不知道怎么在 Haskell 中实现 Peano 数,那你知道 Peano 数是什么,也能大差不差知道答案了。

Peano 数

Peano 数是什么?Peano 数是归纳定义的自然数,准确地说应该是一个表现形如直觉中「自然数」的公理系统,也就是「自然数」的形式化。这个系统里只有两个符号,Zero——表示 0,以及 Succ——表示后继。那么 1 就是 Succ<Zero>2 就是 Succ<Succ<Zero>>,以此类推(归纳,其实就是「以此类推」的形式化)。
我们可以在 C++ 中如此表述:

struct Peano {};
struct Zero: Peano {};
template<class T>
struct Succ: Peano {};

那么加法又是什么呢?从例子出发,我们需要定义一个两个类型参数的模板:

template<class T1, class T2>
struct Add {
  using type = ???;
}

满足直觉中的运算规律,比如 2+1=3,翻译成 C++ 就是 Add<Succ<Succ<Zero>>, Succ<Zero>>::type = Succ<Succ<Succ<Zero>>>。当然类型之间没有等于号,准确地说应该用 std::is_same<T1, T2>,这其实也是通过特化实现的,比如(示意,非官方实现):

template<class T, class U>
struct is_same {
  static constexpr bool value = false;
};
 
template<class T>
struct is_same<T, T> {
  static constexpr bool value = true;
};

那么如何定义加法呢?对于有限的元素,我们当然可以为每一个实例做特化,比如:

template<>
struct Add<Succ<Succ<Zero>>, Succ<Zero>> {
  using type = Succ<Succ<Succ<Zero>>>;
}

也就是打表。C++ 编译器的模板深度一般都是有限的,所以这理论上是可以在实际操作中覆盖所有用例的。但是这明显太傻了。其实加法的定义只需要两条规则就可以覆盖:0 + b = b, (Succ a) + b = Succ (a + b)。翻译成 C++ 就是:

template<class T1, class T2>
struct Add;

template<class T>
struct Add<Zero, T> {
  using type = T;
};

template<class T1, class T2>
struct Add<Succ<T1>, T2> {
  using type = Succ<typename Add<T1, T2>::type>
};

注意那个 typename,gcc 并不知道后面那个 ::type 成员是类型还是变量,所以需要 typename关键字的提示。
这就算写完了,你可以测试看看,是不是满足 std::is_same<Add<Succ<Succ<Zero>>, Succ<Zero>>::type, Succ<Succ<Succ<Zero>>>>::value == true(需要 <type_traits> 头文件,或者上面自己写的那个模板(那就不用加 std::)。这么嵌套着写 Succ 太繁琐了,也不方便看,你可以简单地写一个模板来从整数生成类型:

template<int v>
struct peano {
  using type = Succ<typename peano<v - 1>::type>;
};

template<>
struct peano<0> {
  using type = Zero;
};

然后就可以去验证 Add<<peano<2>::type, peano<1>::type>::type 是不是等于 peano<3>::type 了。
至于加减乘除的其他运算,比较啊奇偶性啊其他的函数,只要你懂得了加法,恐怕就不难了。

练习:

Peano numbers | Codewars 完成加减乘除、奇偶性和比较大小的撰写,并通过测试。

广告时间:

Codewars.com 是一个很好的综合性、游戏化 OJ,除了算法(多是入门级的)之外,考察语言特性(较为深入)是其一大亮点,同时有很多 Haskell 方面的内容,包括我们喜闻乐见的读论文然后完形填空。题图是 Codewars 上上述练习的界面截图。

后记

当然我们还可以进一步「证明」我们印象中的结论,比如加法是满足交换律的,加法和乘法是满足分配率的,等等。这就是后话了。

Like
Like Love Haha Wow Sad Angry
4

一个不正确的 TypeScript 类型定义引发的血案

Like
Like Love Haha Wow Sad Angry
4

Mongoose 的 TypeScript 类型定义并不完全正确!

昨天晚上在 bgs 研究了(或者说调试了)XGS 写的 nodejs 服务器项目。他在用 koa 作为服务器,mongodb 作为数据存储,写一个不知道什么项目(似乎名为「写作者平台」/ “Write’s Platform”)。反正当时还处于很初级的阶段,只有两个路由,一个 get,一个 post,后者是用来向 mongodb 提交一篇文章的。在测试过程中,他发现了一个问题——他在保存文章的回调中更改了响应的内容(如下),但是实际测试得到的响应内容却是 koa 默认的 OK。

router.post('/path', (ctx, next) => {
    const article = new Article( /* something */ );
    article.save((err, article) => {
      if (err){
        ctx.body = err;
      } else {
        ctx.body = "success";
      }
    });
})

起初我发现,他的代码中,router.post('/path', (ctx, next) => { /* body */ });/* body */这段中并没有返回 next(),这应该会导致异步的回调没有被阻塞。

接着我想,可能又是 koa 的版本搞混了什么的,因为 koa v2,koa router 什么的版本管理都很混乱,长期处于虽然大家都想使用最新版,但是默认的却一直是旧版的情况。后来又出现了 koa 是新版,router 是旧版的情况,曾经我为别人解决过这个问题。

但是这次却不是。并且把返回 promise 切换为使用 async/await之后(如下),情况依然没有好转。

router.post('/path', (ctx, next) => {
    const article = new Article( /* something */ );
    return article.save((err, saved) => {
      if (err){
        ctx.body = err;
      } else {
        console.log(saved);
        ctx.body = "success";
      }
      // or `return next();` here.
    }).then((thing) => {
        console.log(thing);
        return next();
    });
})

(过程中我们发现这个传给回调的 thing居然是undefined,这是个伏笔)

渐渐地我被各种函数的签名(比如究竟应该返回一个什么,是 next() 还是一个 promise,而 next 的类型又是什么?)搞得烦躁起来。于是我提议使用 TypeScript 来辅助检查。

我们开始怀疑 article.save这个函数。因为在 koa 官方(仅有)的、涉及到等待异步动作的 router 代码样例里,完全类似的代码是可以工作的:

router.get(
  '/users/:id',
  function (ctx, next) {
    return User.findOne(ctx.params.id).then(function(user) {
      ctx.user = user;
      return next();

    });
  },
  function (ctx) {
    console.log(ctx.user);
    // => { id: 17, name: "Alex" }
  }
);

于是我们着重检查了 mongoose 的 TypeScript 类型定义文件。这个 articleArticle的实例,而Article实际上是 mongoose 动态构造出来的一个类(应该是通过了某种元编程技术)。在类型定义中是这么反映的:

interface Model<T extends Document> extends NodeJS.EventEmitter, ModelProperties {
    new(doc?: Object): T;
    /* other things */
}

而 article 就是那个<T extends Document>,而 Documentsave方法是这么写的:

    /**
     * Saves this document.
     * @param options options optional options
     * @param options.safe overrides schema's safe option
     * @param options.validateBeforeSave set to false to save without validating.
     * @param fn optional callback
     */
    save(options?: SaveOptions, fn?: (err: any, product: this, numAffected: number) => void): Promise<this>;
    save(fn?: (err: any, product: this, numAffected: number) => void): Promise<this>;

(取自 npm install @types/mongoose的结果,不知为何与 DefinitelyTyped 上的有所不同。)看到这,特别是Promise<this>,我就确信了,这个函数没有问题。可是上面返回的thing又确乎是一个undefined,难道是我们的使用姿势有问题?

万般无奈之中,TypeScript 也不靠谱之际,我们只能诉诸源码了。鉴于 mongoose 的代码风格,直接搜索 .prototype.save就能找到。它实际上是在这里,但是看到这里让人更崩溃了:

Model.prototype.save = function(options, fn) {
  if (typeof options === 'function') {
    fn = options;
    options = undefined;
  }

  if (!options) {
    options = {};
  }

  if (fn) {
    fn = this.constructor.$wrapCallback(fn);
  }

  return this.$__save(options, fn);
};

它返回的是this.$__save的结果,但这个函数并没有返回值!日志显示,这里的this就是这个article实例(一个Document)。然而线索到这里似乎断了。怎么能凭空从一个没有返回值的函数返回一个 promise 呢?

我们又想到了一招:查看调用栈。但是 node 的调试器在这时却不好使了。我们只能用原始粗暴的console.trace手动搞出调用栈。虽然没有直接的线索,这个调用栈给了我们一些提示——save并不是直接调用的,而是经过了某种hook的封装。

这时,我想到了直接console.log(article.save),它居然是一个wrappedPointCut出来的结果!再在代码里搜索wrappedPointCut就非常接近真相了。关键的代码在这里

model.prototype[pointCut] = (function(_newName) {
      return function wrappedPointCut() {
        var Promise = PromiseProvider.get();

        var _this = this;
        /* something omitted for simplicity */
        var promise = new Promise.ES6(function(resolve, reject) {
          args.push(function(error) {
            if (error) {
              /* something omitted for simplicity */
              reject(error);
              return;
            }
            $results = Array.prototype.slice.call(arguments, 1);
            resolve.apply(promise, $results);
          });
          _this[_newName].apply(_this, args);
        });
        if (fn) {
          /* something omitted for simplicity */
          return promise.then(
            function() {
              process.nextTick(function() {
                fn.apply(null, [null].concat($results));
              });
            },
            function(error) {
              process.nextTick(function() {
                fn(error);
              });
            });
        }
        return promise;
      };
    })(newName);

注意到如果没有fn,那么会在第 33 行返回第 7 行定义的 promise,如果有fn,就会直接在第 21 行返回接续之前的 promise 的另一个 promise,这个接续(.then())里面利用了fn处理了结果,却没有返回。也就是说,这时候的返回值是Promise<undefined>而不是Promise<this>——我们被这样的 TypeScript 类型定义坑惨了!如果它一开始就告诉我们会这样,那该多好呀,就像这样:

    save(options: SaveOptions, fn: (err: any, product: this, numAffected: number) => void): Promise<undefined>;
    save(fn: (err: any, product: this, numAffected: number) => void): Promise<undefined>;
    save(options?: SaveOptions): Promise<this>;
    save(): Promise<this>;

2017 年 5 月 4 日

Like
Like Love Haha Wow Sad Angry
4