Jai编程语言,作者Jonathan Blow

2021-01-21 04:35:45

Permalink Jai是由独立游戏《编织》和最近的《见证》的创造者乔纳森·布洛(Jonathan Blow)开发的一种高级编程语言。它是一种强制性的静态/强类型C语言,但是具有C缺少的各种现代语言功能。 Blow于2014年9月下旬开始在Jai上工作。它仍在开发中,到目前为止尚未向公众提供。 Blow着眼于视频游戏来开发它,但实际上,它是一种通用的编程语言,可以用于任何任务。

免责声明:我与Jon Blow没有关系。在撰写本文时,Jai还没有公共编译器,因此本文中的所有信息均来自他的YouTube视频。因此,这篇文章中没有任何内容是官方的。可能有比此页面上更多的最新信息。就是说,我相信这篇文章中的所有内容在撰写本文时都是最新的。 (如果您是Jon Blow,并且希望我更正本文中的任何内容,我将非常高兴)。

除非另有说明,否则本文档中的所有内容均已实现并且当前在(当前私有)原型中运行。由于尚未发布,因此本文档中的所有内容均可能更改。

简而言之,Jai可以说是C的现代替代品。一些最酷的功能:

任意编译时代码执行–可以使用#run使程序的任何功能在编译时运行

语法促进的代码重构–语言语法使代码易于从本地块→本地功能→全局功能移动,从而促进了代码重用。

集成的构建过程–构建过程和参数由源代码本身指定,以保持一致性

面向数据的结构–数组结构和结构数组之间的自动转换,避免了类和继承

反射和运行时类型信息–运行时可用的每个结构的静态类型信息

一种新的多态过程方法–函数级的多态,由程序员通过特殊过程控制

低级内存管理工具–更好地控制库分配内存的方式,自动所有权管理,无垃圾收集

明确控制优化和性能特征–明确控制内联,边界检查和初始化

在编程多年之后的某个时刻,“令人兴奋的编程冒险”与“请不要进行其他代码重构”之间的界线可能会开始消失。更改签名时,必须更新标头中的函数声明会很快变老。当C最初于1973年编写时,所有这些标头材料都有充分的理由,但今天没有。

语言提供的生活质量改善可以为使用该语言的程序员的生产力带来可量化的收益。 (如果您不满意,请尝试使用Brainfuck进行任何编程。)如果不是即时的,则编译应该很快,重构代码应该需要最少的更改,并且错误消息应该是有益而愉快的。人们相信,程序员使用的工具的改进可以使生产率提高20%以上,这是创建新语言的主要动机。

最初的设计师说,视频游戏是充满内存的机器。大多数时候,游戏程序员都在考虑如何以允许有效访问和处理数据的方式用大量数据填充内存。必须将数百兆的内存从硬盘移动到主内存,再从那里移动到视频卡或处理器高速缓存,然后进行处理并返回到主内存。由于视频游戏玩家不愿意等待,因此所有这些操作都必须按照我们的宇宙定律允许的最快速度完成。编程语言的主要目的是允许指定算法来管理数据。垃圾收集,模板化数据流和动态字符串类之类的语言功能可能有助于程序员更快地编写代码,但并不能帮助程序员更快地编写代码。

Jai的另一个主要设计目标是减少编程时的摩擦。当一种语言的语法干扰程序员的工作流程时,就会发生摩擦。例如:

Java要求所有对象都是类,从而迫使程序员将所需的全局变量放入全局类中。

C ++的lambda函数语法与它的类方法语法不同,而类方法语法本身与其全局函数语法不同。

Java,Haskell和C ++是所谓的“大议程”语言的示例,其中的语言理想主义(在C ++的情况下,它缺乏一致的愿景)妨碍了程序员的发展。 Jai的摩擦公差低,尤其是在不必要的情况下。

Jai是为优秀程序员而设计的语言,而不是针对不良程序员的语言。像Java这样的语言在市场上都是防白痴的,因为程序员编写会伤害到他们的代码要困难得多。 Jai的理念是,如果您不想让白痴为您的项目编写错误的代码,那么就不要雇用任何白痴。 Jai允许程序员直接使用锋利的工具来完成工作。游戏程序员不怕指针和手动内存管理。程序员确实会犯错误并导致崩溃,甚至可能导致严重的崩溃,但是这种说法是,缺少内存安全机制时,生产率的提高和摩擦的减少比弥补跟踪错误所浪费的时间要多,尤其是当优秀的程序员时倾向于产生相对较少的错误。

如果作为程序员您在乎用户体验(应该这样做),那么您应该在意程序的性能。您应该在要运送的一系列机器上考虑代码的行为,并设计数据和控制结构以最有效地利用该硬件的功能。 (在这里,我描述的是Mike Acton的“面向数据的设计”方法。)那些关心其目标硬件上软件性能的程序员会受到位于他们和硬件之间的编程语言的束缚。虚拟机和自动内存管理等机制会干扰程序员推断目标程序在目标硬件上的性能的能力。发明了RAII,构造函数和析构函数,多态性和异常之类的抽象,目的是解决游戏程序员所没有的问题,并且干扰了游戏程序员所遇到的问题的解决方案。 Jai抛弃了这些抽象,以便程序员可以更多地考虑他们的实际问题-数据和算法。

语法名称:type = value;指定名称为name的变量的类型类型,并将接收值值。它是由肖恩·巴雷特(Sean Barrett)提出的。一些例子:

计数器:= 0; //一个intname:=" Jon&#34 ;; //字符串平均值:= 0.5 *(x + y); //一个浮点数

所有这些可能都比您习惯的落后,但是学习曲线很浅,您很快就习惯了。函数声明如下所示:

//一个接受3个浮点数作为参数并返回floatsum的函数::(x:float,y:float,z:float)-> float {return x + y + z;} print(" Sum:%\ n&#34 ;, sum(1,2,3));

答:[50] int; //由50个整数组成的数组b:[..] int; //动态整数数组

数组不会像C中那样自动转换为指针。它们是包含数组大小信息的“宽指针”。函数可以采用数组类型并查询数组的大小。

print_int_array ::(a:[] int){n:= a。计数;对于i:0 .. n-1 {print(" array [%] =%\ n&#34 ;, i,a [i]); }}

保留数组大小信息可以帮助开发人员避免将数组长度作为附加参数传递的方式,并有助于自动边界检查(请参阅Walter Bright – C的最大错误)。

假设我想用C编写一个将线性颜色值转换为sRGB的函数。这涉及到pow()函数,这是昂贵的一面。我们可以自己进行计算,并将结果作为程序的一部分进行分发,从而避免使用pow()。因此,我们编写了一个值表并将其返回。

#define SRGB_TABLE_SIZE 256 float srgb_table [SRGB_TABLE_SIZE] = {/ * ...这里的值... * /} float linear_to_srgb(float f){//在我们的表格中找到此SRGB值的索引,//假设f在范围[0,1] int table_index =(int)(f * SRGB_TABLE_SIZE);返回srgb_table [table_index];}

(注意:上面的代码是错误的,仅用于示例。要获得更好的代码,请尝试使用stb_image_resize的sRGB函数。)到目前为止,一切都很好,除了我们将如何获得srgb_table的值之外?我们可以编写另一个输出值的小程序。例如:

float real_linear_to_srgb(float f){if(f< = 0. 0031308f)return f *12。92f;否则返回1. 055f *(float)pow(f,1 /2。4f)-0。055f;}#define SRGB_TABLE_SIZE 256 int main(int c,char * s){printf(" float srgb_table [SRGB_TABLE_SIZE ] = {");对于(int i = 0; i< SRGB_TABLE_SIZE; i ++)printf("%f,&#34 ;, real_linear_to_srgb((float)i / SRGB_TABLE_SIZE)); printf("} \ n");返回0;}

我们可以编译这个小程序,它将输出一个sRGB值表,然后将输出复制到我们的实际程序中。

这是一大问题。例如,注意如何两次定义SRGB_TABLE_SIZE,一次在实际程序中,一次在助手程序中。因此,我们现在必须维护两个单独的源代码。对于大型程序,这可能会变得很笨拙。

generate_linear_srgb ::()-> [] float {srgb_table:float [SRGB_TABLE_SIZE]; srgb_table {<<它= real_linear_to_srgb(cast(float)it_index / SRGB_TABLE_SIZE)} return srgb_table;} srgb_table:[] float = #run generate_linear_srgb(); // #run调用编译时间执行real_linear_to_srgb ::(f:float)->浮动{table_index:= cast(int)(f * SRGB_TABLE_SIZE);返回srgb_table [table_index];}

#run指令指示Jai在编译时运行函数generate_linear_srgb()。 Jai的编译时函数执行在编译时运行该命令,并返回一个值表,然后将该表直接编译为srgb_table的二进制文件。运行程序时,generate_linear_srgb()函数不再存在。只有它生成的表存在,由linear_to_srgb()使用。

编译时函数的执行几乎没有限制。实际上,您可以在代码库中运行任意代码作为编译器的一部分。 Jai的第一个演示显示了如何作为编译器的一部分运行整个游戏,以及如何将游戏中的数据烘焙到程序二进制文件中。 (我希望#run invaders();随语言一起提供。)编译器将编译时执行的函数构建为特殊的字节码语言,并在解释器中运行它们,然后将结果集中到源代码中。然后,编译器继续正常运行。

与您在火星上的火星探测器交谈,等待数据包返回并获得火星外观的照片

所有代码都在这样的某种代码块中开始其生命,然后再继续用于更一般的情况。 Jai具有一些特殊的语法,可以帮助程序员将代码从特定情况转移到一般情况,以促进代码重用。

例如,假设您正在编写如下代码:

draw_particles ::(){view_left:Vector3 = get_view_left(); view_up:Vector3 = get_view_up();用于粒子{//在for循环中," it" object是当前对象的迭代器。 particle_left:= view_left *它。粒子大小; particle_up:= view_up *它。粒子大小; // m是一个全局对象,可帮助我们构建网格以发送给图形API m。 Position3fv(原点-粒子左-粒子上);米Position3fv(原点+粒子左-粒子上);米Position3fv(原点+左粒子+上粒子);米Position3fv(原点-粒子左+粒子上); }}

这些网格生成调用实际上是某些常规四边形渲染的特例,因此可以将它们分解为另一个函数,以便在其他地方使用。 Jai使得此重构非常简单。第一步是使用特殊的捕获语法将代码包含在新范围内。

particle_left:= view_left * it.particle_size; particle_up:= view_up * it.particle_size; origin:= it.origin; [m,origin,particle_left,particle_up] {m。 Position3fv(来源-粒子左-粒子上);米Position3fv(原点+粒子左-粒子上);米Position3fv(原点+粒子左+粒子上);米Position3fv(来源-粒子左+粒子上);}

(免责声明:尚未执行此步骤。这是计划的功能之一。)[m,origin,particle_left,particle_up]表示法是一种捕获,可防止在捕获对象的内部范围内访问捕获中未包含的任何对象。新的支架。请注意,我们必须将它更改为origin,并将其更改为origin,并将origin添加到捕获列表中—它没有被捕获,并且在内部范围内不可用。

捕获有助于重构代码,如我们在此处看到的,但它们也可以通过其他方式提供帮助。例如,当程序员将代码从单线程转移到多线程时,捕获可能会强制只访问线程本地数据。捕获是一种保险单,捕获内的代码仅读取或写入捕获中指定的状态。

现在,我们已经确定了代码中依赖于外部事物的所有部分,因此,我们改善了代码的卫生性,并使将代码轻松应用于自身功能变得容易。现在我们要继续,以便可以在其他地方使用四边形绘图代码。因此,我们从此块捕获中创建一个函数:

particle_left:= view_left * it.particle_size; particle_up:= view_up * it.particle_size; origin:= it.origin;()[m,origin,particle_left,particle_up] {m。 Position3fv(来源-粒子左-粒子上);米Position3fv(原点+粒子左-粒子上);米Position3fv(原点+粒子左+粒子上);米Position3fv(origin-粒子左+粒子上);}(); //调用函数

请注意,我们唯一需要做的更改就是添加函数语法()。捕获保持不变。因此,我们只需花费很少的精力就可以将捕获的捕获从功能变为函数。现在,如果我们愿意,可以将向量移动为函数参数:

(来源:Vector3,左:Vector3,上:Vector3)[m] {m。 Position3fv(原点-左-上);米Position3fv(来源+左-上);米Position3fv(原点+左+上);米Position3fv(来源-左+上);}

使用参数名称,我们可以在函数作用域内更改变量的名称,以匹配其新函数。现在我们可以使用此函数绘制任何类型的四边形,而不仅仅是粒子。捕获保留m,因为它是一个全局对象,不需要作为参数传递。现在我们有了一个匿名的,局部作用域的函数,可以在我们的绘制代码中使用它:

draw_particles ::(){view_left:Vector3 = get_view_left(); view_up:Vector3 = get_view_up();对于粒子{particle_left:= view_left *它。粒子大小; particle_up:= view_up *它。粒子大小; (来源:Vector3,左:Vector3,上:Vector3)[m] {m。 Position3fv(原点-左-上);米Position3fv(来源+左-上);米Position3fv(原点+左+上);米Position3fv(来源-左+上); }(来源,particle_left,particle_up); //使用指定的参数调用该函数}}

匿名函数对于作为参数传递给其他函数很有用,这种语法使它们易于创建和操作。下一步是给我们的函数起一个名字:

draw_quad ::(来源:Vector3,左:Vector3,上:Vector3)[m] {m。 Position3fv(原点-左-上);米Position3fv(来源+左-上);米Position3fv(原点+左+上);米Position3fv(原点-左+上);} draw_quad(原点,粒子左,粒子上);

现在,如果愿意,我们可以在本地范围内多次调用它。但是我们要从全局范围访问四边形绘制函数。将功能移出本地范围需要对功能代码进行零更改:

draw_quad ::(来源:Vector3,左:Vector3,上:Vector3)[m] {m。 Position3fv(原点-左-上);米Position3fv(来源+左-上);米Position3fv(原点+左+上);米Position3fv(origin-left + up);}; draw_particles ::(){view_left:Vector3 = get_view_left(); view_up:Vector3 = get_view_up();对于粒子{particle_left:= view_left *它。粒子大小; particle_up:= view_up *它。粒子大小; draw_quad(particle_left,particle_up,origin); }}

Jai函数语法的优势在于它不会改变该函数是匿名函数,本地函数(即位于另一个函数范围内),类的成员函数还是全局函数。这与在C ++中相反,在C ++中,局部函数称为lambda,并且与成员函数具有完全不同的语法,成员函数必须具有类名和::等,这与不具有成员函数的全局函数的语法略有不同。类名或::。结果是,随着代码的成熟和从本地上下文移动到全局上下文,重构工作可以通过最少的编辑来完成。

{...} //匿名代码块[捕获] {...} //捕获的代码块(i:int)-> float [capture] {...} //匿名函数f ::(i:int)-> float [capture] {...} //命名为局部函数f ::(i:int)-> float [capture] {...} //命名为全局函数

用于构建程序的所有信息都包含在该程序的源代码中。因此,不需要make命令或项目文件来构建Jai程序。作为一个简单的例子:

build ::(){build_options。可执行文件名称="我的程序&#34 ;;打印("构建程序' \ n&#34 ;, build_options。可执行文件名); build_options。 Optimization_level = Optimization_Level。调试; build_options。 embed_line_directives = false; update_build_options(); // Jai会自动构建#load指令中包含的所有文件,//但也可以手动添加其他文件。 add_build_file(" misc.jai"); add_build_file(" checks.jai");}#run build();

生成程序时,#run指令在编译时运行build()。然后build()为该项目建立所有构建选项。不需要外部构建工具,所有构建脚本都在Jai内完成,并且与其余代码在同一环境中完成。

遵循空间局部性,现代处理器和内存模型将更快。这意味着将同时修改的数据分组在一起对于性能而言是有利的。因此,从结构数组(AoS)样式更改结构:

struct Entity {Vector3 position;四元数取向; // ...这里有许多其他成员};实体all_entities [1024]; //(int k = 0; k <1024; k ++)的结构数组update_position(&amp; all_entities [k] .position); for(int k = 0; k <1024; k ++)update_orientation(&amp; all_entities [k] .orientation);

struct Entity {Vector3 position [1024]; 四元数方向[1024]; // ...这里还有许多其他成员};实体all_entities; //(int k = 0; k&lt; 1024; k ++)的数组的结构update_position(&amp; all_entities.positions [k]); for(int k = 0; k <1024; k ++)update_orientation(&amp; all_entities.orientations [k]); 但是,随着程序的变大,重组数据变得更加困难。 测试单个简单更改是否会对性能产生影响可能会花费开发人员很长时间,因为一旦数据结构必须更改 ......