写点什么

编程语言 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:227759

评论

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

MyBatis实现动态SQL更新

越长大越悲伤

Java mybatis mybatis的拦截器 Mybatis Plus

小程序开发这么简单,还需要App吗?

FinFish

小程序生态 APP开发 小程序开发

Boundless Hackathon @Stanford 主题黑客松活动闭幕,一文回顾

股市老人

《面试1v1》HashMap

JavaPub

Java jdk JVM hashmap

Boundless Hackathon @Stanford 主题黑客松活动闭幕,一文回顾

鳄鱼视界

PyTorch模型容器与AlexNet构建

timerring

PyTorch

Spring 中的父子容器是咋回事?

江南一点雨

Java spring

2023-07-11:给定正整数 n, 返回在 [1, n] 范围内具有 至少 1 位 重复数字的正整数的个数。 输入:n = 100。 输出:10。

福大大架构师每日一题

Go 算法 rust 福大大架构师每日一题

Docker学习路线2:底层技术

小万哥

Docker 程序员 容器 后端 开发

团队如何选择合适的Git分支策略?

DevOps在路上

git DevOps 分支管理 DevOps工具

悦数图数据库:图技术加速行业大模型智能化应用落地|WAIC 2023 精彩回顾

最新动态

DevOps流水线设计的最佳实践

DevOps在路上

DevOps jenkins Tekton gitlab ci 持续交付流水线

腾讯云AI绘画好不好用,与同类产品对比有何优势?来看看ChatGPT是如何回答的

牵着蜗牛去散步

腾讯云 腾讯 腾讯云AI绘画

软件高效自动化部署:华为云部署服务CodeArts Deploy

云计算 开发者 软件开发 华为云 华为开发者大会

HDC.Cloud 2023 |华为云5大开源项目发布与更新,助力开发者实现应用创新

华为云开源

开源 云原生 HDC.Cloud

es笔记六之聚合操作之指标聚合

Hunter熊

elasticsearch

《面试1v1》开篇

JavaPub

Java redis elasticsearch 面试 计算机

C++之openFrameworks框架

芯动大师

悦数图数据库:图技术加速行业大模型智能化应用落地|WAIC 2023 精彩回顾

悦数图数据库

人工智能 图数据库 国产数据库 大模型 AIGC

开源铸剑,五载匠心!Zilliz Cloud云服务盛装登场,引领向量数据库云时代!

Zilliz

阿里云 SaaS 云服务 Zilliz zillizcloud

一个提高go开发效率的秘密武器,一天开发完成一个极简版社区后端服务

vison

crud gorm gin #go 代码自动生成

你以为搞个jenkins流水线每天跑,团队就在使用CI/CD实践了?

DevOps在路上

DevOps 持续集成 jenkins

实操:用Flutter构建一个简单的微信天气预报小程序

没有用户名丶

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