【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

编程语言 Zig 有什么与众不同的

  • 2022-11-10
    北京
  • 本文字数:3720 字

    阅读完需:约 12 分钟

编程语言Zig有什么与众不同的

Zig 允许在编译期执行代码,这有什么意义?



Zig 的吉祥物“零号(Zero the Ziguana)”


编程语言专家曾对 Zig 编程语言的创造者 Andrew Kelley 说,在编译时运行代码是个蠢主意。尽管如此,Kelley 还是去实现了这个想法,而多年以后,这个蠢主意已经成为了 Zig 的招牌。这一特征在 Zig 中用关键字 comptime 标识,代表需要在编译时运行的代码或者是需要的变量。Zig 可以在编译时运行代码的能力让开发者们可以在不明确任何泛型或模板支撑的情况下,编写通用代码或是进行元编程。让我们来通过代码例子更直观地了解编译时运行是什么意思,以及其为什么重要。以这段简单的函数为例,在 a 和 b 两个数之间取最大值。不使用泛型或 comptime 代码的话,我们就需要将这个函数的具体变量类型写死,比如这里用的 Zig 中 32 位整数 i32 。


fn maximum(a: i32, b: i32) i32 {    var result: i32 = undefined;
if (a > b) { result = a; } else { result = b; }
return result;}
复制代码


和 C/C++ 一样,Zig 中可执行的程序通常都会有个 main 函数,我们可以在主函数里面调用最大值函数。在下面的代码,暂时不用管 stdout 的调用或者在 print 函数前的 try 关键词,后者和 Zig 的错误处理有关,在本文中并不涉及。


pub fn main() !void {    const stdout = std.io.getStdOut().writer();
const a = 10; const b = 5;
const biggest = maximum(a, b);
try stdout.print("Max of {} and {} is {}\n", .{ a, b, biggest });}
复制代码


很明显,这个解决方案有很大局限性。首先,maximum 只能处理 32 位整数。C 语言编程者大概对这个问题并不陌生,C 预处理的宏就是用来解决这个问题的。Andrew Kelley 为避免依赖 C 的宏,专门设计了 Zig。可以说,Zig 存在的原因本质上就是 Andrew 想用 C 编程,但又不想折腾宏这类烦人的东西。comptime 的诞生的意义完全就是为了取代 C 的宏。


让我们再看看 Zig 对这类问题的解决方案。先在 Zig 中定义一个泛型 maxiumum 函数,用 anytype 和 @TypeOf(a) 替代 i32 类型参数。在 maximum 函数在被调用时,将默认 anytype 为提供的参数类型。请注意,Zig 不是动态编程语言,在用不同参数类型调用 maximum 时,Zig 的编译情况也会不同。a 和 b 的类型依旧会在编译时决定,而非运行时。


虽然在编译时确定输入参数的类型不是不行,但这么一来变量和返回类型就难处理了。anytype 不能用作是返回类型,因为我们不能在函数调用处再确定变量的具体类型。因此,我们需要用编译器内联函数 @TypeOf 在编译时生成返回类型,比如用 @TypeOf(a) 在编译时确定参数 a 的类型,或者是用来指定返回变量 result 的类型:


fn maximum(a: anytype, b: anytype) @TypeOf(a) {    var result: @TypeOf(a) = undefined;
if (a > b) { result = a; } else { result = b; }
return result;}
复制代码


虽然确实有了一定的提升,但还有别的问题:


  1. 没有限制用非数字参数调用 maximum 的情况

  2. 如果 b 值更大,那么返回值会有会超出 @TypeOf(a) 范围的情况


要想检测 a 和 b 的类型是否正确,我们可以创建一个在编译时运行的函数来检测参数是否是数字。定义函数 assertNumber 只有一个代表类型的参数 T,参数之前加上的 comptime,告诉编译器这是要在编译时必须已知的参数。


另外还需要注意下 switch 条件语句。在 Zig 里,switch 也可以返回数值,因此我们用参数 T 的类型做开关,如果 T 符合数字类型,那么 switch 条件语句就会返回 true,并将其赋给 is_num 变量。非数字类型则用 else 默认返回 false。


fn assertNumber(comptime T: type) void {    const is_num = switch (T) {        i8, i16, i32, i64 => true,        u8, u16, u32, u64 => true,        comptime_int, comptime_float => true,        f16, f32, f64 => true,        else => false,    };
if (!is_num) { @compileError("Inputs must be numbers"); }}
// testing functionpub fn main() !void { assertNumber(bool);}
复制代码


在这个函数定义中另一个值得关注的点是 @compileError ,一个用来将编译器错误信息返回给用户的编译时内联函数。在这段代码中,我们给参数 assertNumber 提供了非数字的类型 bool,尝试编译这段程序后,我们会收到以下这段错误信息:


assert-number.zig:11:9: error: Inputs must be numbers        @compileError("Inputs must be numbers");        ^assert-number.zig:17:17: note: called from here    assertNumber(bool);                ^assert-number.zig:16:21: note: called from herepub fn main() !void {
复制代码


也就是说,我们可以在运行无效代码时,用代码本身给用户输出更加有价值的错误信息。下面让我们用 assertNumber 检查 maximum 函数的输入。为保证返回类型范围足够,我们可以让两个输入参数类型必须相同:


fn maximum(a: anytype, b: anytype) @TypeOf(a) {    const A = @TypeOf(a);    const B = @TypeOf(b);
assertNumber(A); assertNumber(B);
var result: @TypeOf(a) = undefined;
if (A != B) { @compileError("Inputs must be of the same type"); }
if (a > b) { result = a; } else { result = b; }
return result;}
复制代码


在运行时调用 maximum 会替换用编译结果替换所有编译时代码。但目前这种解决方案还没有解决我们原始函数的所有问题。我们强制使 a 和 b 保持同样的类型,那么如果我们想要对比有符号的 8-bit 和有符号的 32-bit 整数,也就是 Zig 中的参数类型 i8 和 i32 呢?那么我们就必须保证返回类型是 i32,目前的方案并不能做到这一点。我们需要的是一个能够在编译时运行,对比 a 与 b 的类型,并返回最长比特类型的函数。


想做到这点,那么我们还需要以下两个函数:


  • nbits 函数,用于计算类型 T 的比特长度

  • largestType 函数,用于返回 A 和 B 两个类型中比特最长的一个


注意在下面的这个例子中我们用了 comptime 来标记参数的类型,以告知 Zig 这些输入在编译时必须已知,编译器内联函数 @typeInfo 用于在编译时返回用于描述类型的复合对象 info,其中包含了类型是否带符号,类型需要多少比特来表示的信息。


fn nbits(comptime T: type) i8 {    return switch (@typeInfo(T)) {        .Float => |info| info.bits,        .Int => |info| info.bits,        else => 64,    };}
fn largestType(comptime A: type, comptime B: type) type { if (nbits(A) > nbits(B)) { return A; } else { return B; }}
fn maximum(a: anytype, b: anytype) largestType(@TypeOf(a), @TypeOf(b)) { var result: @TypeOf(a) = undefined;
if (a > b) { result = a; } else { result = b; }
return result;}
复制代码


可能例子里的 switch 语句表示得不是很清楚,让我再解释下。@typeInfo(T) 所返回的类型是联合类型(union type)std.builtin.TypeInfo ,这种类型和结构(struct)有些相似,都包含多个共享内存的字段。因此我们需要使用 switch 条件语句找到具体是在使用.Int 还是.Float 字段。|info|语法在 Zig 中是用来解包数值的,在这里我们用它来找描述类型的结构。info 对象会有两种类型 TypeInfo.Int 或者 TypeInfo.Float,但这两种 struct 类型都会有一个 bits 字段。在我们改进后的 maximum 函数里,我们没有明确指定返回值,而是调用了 largestType 函数并将它的返回值用做了 maximum 返回值的类型。尽管看起来很怪,但这确实是可行的,因为 Zig 编译器在编译时调用 largestType 的确只依赖了已知信息。编译器会根据每次 maximum 的调用创建不同变体,对不同的输入类型和输出类型进行编译。


用编译时的代码实现泛型


Zig 中 comptime 的强大可以通过对泛型的实现来证明。在下面的例子中的 minimum 函数对习惯于泛型或基于模板编程的开发者来说很是熟悉。其中的关键区别在于,类型参数 T 是作为一般参数输入的。对于 C++、Java 和 C# 的开发者来说,这个函数一般会以 minimum(x, y) 的形式调用,但对于 Zig 开发者来说,minimum(i8, x, y) 足矣。


fn minimum(comptime T: type, a: T, b: T) T {    assertNumber(T);
var result: T = undefined; if (a < b) { result = a; } else { result = b; }
return result;}
复制代码


在 C/C++、Java 或 Swift 等语言中,我们通常可以从输入参数中推断变量类型。但在 Zig 中,这种类型推断不再可行,因为参数 T 被用作为一般参数,得不到特殊待遇了。虽然这让 comptime 弱势于泛型,但好处是 comptime 用起来更加灵活了。我们可以用 comptime 代码定义泛用类型,比如我们可以用 2D 矢量类来表示力、速度以及位置等信息。


查看英文原文:


What Makes the Zig Programming Language Unique? by Erik Engheim(https://erikexplores.substack.com/p/what-makes-the-zig-programming-language)


声明:本文为 InfoQ 翻译,未经许可禁止转载。


2022-11-10 19:226921

评论

发布
暂无评论
发现更多内容

《我们脑中挥之不去的问题》 - 卓克科普(2)

石云升

读书笔记 科普 2月春节不断更

性能压测的时候,随着并发压力的增加,系统响应时间和吞吐量如何变化,为什么?

跳蚤

Elasticsearch Mapping Overview

escray

elastic 七日更 死磕Elasticsearch 60天通过Elastic认证考试 2月春节不断更

架构师训练营 4 期 第7周

引花眠

架构师训练营 4 期

日记 2021年2月14日(周日)

Changing Lin

2月春节不断更

【LeetCode】情侣牵手Java题解

Albert

算法 LeetCode 2月春节不断更

浅谈性能优化

跳蚤

Tomcat速查手册

jiangling500

Java tomcat

JDBC速查手册

jiangling500

Java JDBC

SpringMVC专栏 第1篇 - 快速入门

小马哥

Java spring Spring MVC 七日更 二月春节不断更

熬夜总结了 “HTML5画布” 的知识点(共10条)

我是哪吒

学习 读书笔记 程序员 随笔杂谈 2月春节不断更

盘点关于程序员的那些经典案例

孙叫兽

程序员 程序人生 话题讨论 薪水 计算机原理

深入浅出函数式编程:Stream流水线的实现原理

码农架构

Java 架构 微服务

杨明越:Kubernetes的下一仗可能是提升标准化程度

杨明越

聊聊大公司创新的机制:饱和攻击

boshi

创新 七日更

Spring框架源码:BeanFactory与Bean的生命周期

程序员架构进阶

Java spring 源码阅读 七日更 2月春节不断更

gradle中的build script详解

程序那些事

maven Gradle 程序那些事 构建脚本 构建程序

写一个用例(总结)第四周

mas

机器学习·笔记之:

Nydia

还傻傻分不清楚equals和==的区别吗?看完就明白了

codevald

Java 源码分析 string Object

9. Python 学习过程的第一个山坡,99%的人都倒在了山坡下

梦想橡皮擦

Python 2月春节不断更 python入门 python学习

Chrome浏览器多进程架构3个必会知识点

梁龙先森

面试 大前端 浏览器

「架构师训练营 4 期」 第七周 - 001&2

凯迪

架构师训练营 4 期

Scrum Patterns:梳理产品待办列表(译)

Bruce Talk

敏捷开发 译文 Agile Scrum Patterns

《我们脑中挥之不去的问题》- 卓克科普(1)

石云升

读书笔记 科普 2月春节不断更

LeetCode 数据库刷题 - 1179. 重新格式化部门表

小马哥

七日更 二月春节不断更

并发编程系列:线上问题定位

程序员架构进阶

Java 并发 问题排查 七日更 2月春节不断更

日记 2021年2月13日(周六)

Changing Lin

2月春节不断更

Java SE最佳实践

jiangling500

Java 最佳实践 Java SE

第十二周 数据应用一 作业 「架构师训练营 3 期」

胡云飞

【STM32】串口通信---用代码与芯片对话

AXYZdong

硬件 stm32 2月春节不断更

编程语言Zig有什么与众不同的_开源_Erik Engheim_InfoQ精选文章