设计SHT语言
#编程 #python #功能 #language

介绍

我是贸易和业余爱好的程序员,因此,我的大部分时间都花在计算机的前面,沉浸在代码中。每天,我必须使用许多工具,技术,技术和流程,这些工具,技术,技术和过程通常需要大量的研究和追赶。

但是,我从未对每个程序员的主要工具(语言本身)给予过多关注。我的意思是,我时不时地读到了理论,而且我肯定对它们有一些强烈的看法,但是我从来没有学会过为什么他们的内部内容和如何。

这对我来说变得至关重要,因为我想在工作中更有效,但是随着现代编译的编程语言(例如Go and Rust)的兴起,我开始感觉像是该领域的遗物。

那么,如何在编程语言理论方面获得更深入的知识?阅读文档?技术博客?问chatgpt?不,不适合我。最好的学习方法是设计自己的语言,击中人们过去几十年来解决的所有问题,后悔我的决定,然后阅读文档,技术博客,并吸引聊天机器人。

这就是为什么我要使用自己的语言工作的原因:在设计和实施语言时首先学习所有困难。

因此,SHT是一种动态的程序脚本语言,我将其设计为简洁,表现力,直观且有意直接。就像其他设计自己的语言想要的人一样。

您应该使用这种SHT语言吗?如果需要一个精心设计的,经过良好的经过良好的,经过良好的测试,记录的,快速和安全的工具,我的答案是否定的 - 如果名称还不够清晰。但是,如果您是一个很大的书呆子,并且想学习一些有趣的学习工具,这些工具对您来说永远不会有用,请尝试一下。还可以随时与我联系以讨论设计和实施选择。

通过本文的其余部分,我将介绍一些高级决策和整体准则,然后概述该语言的一些关键特征。我不打算深入研究技术细节,既没有带来太多的设计讨论,我希望在以后的文章中讨论这一点。

一般决策和指示

好,所以我想创建自己的语言,现在呢?可能性是无限的,特别适合像我这样无知的人。

为了减少可能的选择数量,我决定创建一种由GO和Python启发的简单语法的语言,其表现力是受功能性语言启发的,尤其是Haskell和F#,并且有了一些想法,我会喜欢尝试一下,主要是受生锈的启发。

鉴于这个总体方向,我想到的第一件事是迭代器。我喜欢迭代器如何带来清晰度 - 抽象其数据结构的内部混乱并提供简单的内容:元素列表。由于这是我参考的主要交集点,因此我决定给他们语言的主角。

由于这是我的第一语言(实际上是第二种语言,但第一个结果真是太快了),所以我认为这很容易拥有最小的类型控件,并且仅在运行时才更容易。我还不知道这是否是完全正确的,但我希望它可以减轻我必须用强壮的类型语言进行的分析。

同样,我决定创建一种动态语言,因为我相信执行语句要比将其翻译成字节代码要容易。我还避免转向中间表示或其他语言,因为我想学习评估步骤的困难。

请注意,我不关心速度,我正在使用树步道的方法来简单,而没有任何优化,这不是我感兴趣的研究领域。为了降低复杂性和范围,我也不会考虑异步执行。

语法系列比Python更接近,因为我相信我不想解决的基于标识的范围存在一些固有的问题,但是Python语法也适合该语言。此外,我想添加一些功能上的功能,我认为这是优雅的句法

终于,我将遵循程序范式,因为对象方向是胡扯;)

特征

类型和变量

原始类型简单而通用,有数字,字符串,功能,错误等。可以使用GO的表达来定义变量,并且可以将其重新分配到任何其他值。

# comments are like python
# declarations are like go
number := 1 + 1.5 - 5e-1
string := 'Hello, World'
boolean := true or false
tuples := 1, 2, 3
lists := List { 1, 2, 3 }
hashs := Dict { a:1, b:2 }

# variables can be reassigned
number = 'well, not a number'
a,b := 1,2
lists[0], lists[1] = lists[1], lists[0] 

我考虑过将作业阻止其他类型,但这会增加一个烦人的约束,特别是因为该语言没有零值,并且某些情况需要大量其他工作,例如,如何处理空块和功能?我的解决方案非常丑陋,但很简单:每种情况都会导致无效或未定义的情况,例如空块,默认值将为false

我有目的地删除了null,因为我想看到没有历史上任何语言中运行时错误的主要罪犯的影响。如稍后所述,这将影响错误处理和自定义数据类型,但我怀疑它会使语言更安全,因为类型是动态。

函数自然是头等舱,有两种功能:范围且没有范围(箭头功能),类似于JavaScript:

fn add(a, b) {
  return a + b
}

fn scaler(x) {
  return (a, b) => add(a, b)*x
}

scaler(2)(3, 1) # 8

回报是可选的。函数还接受将自动转换为列表和默认参数的传播参数。

fn headTail(h=false, ...b, t=false) {
  return h, t
}

headTail(1, 2, 3, 4) # (1, 4)

操作员很简单:

# Arithmetic
1 + 2
2 - 3
4 * 5
6 / 7
8 // 9 # int division
10**2
3 % 4

# Boolean
a or b
b and c
!d

# Concat (forcing string convertion)
a .. b .. c

# Meta function dependent
a in b
a is b

我计划 - 并实施-XOR,NAND和NXOR,然后我意识到andor操作员通常具有特殊属性:他们只有在该值会更改表达式结果时评估下一个值(例如false and f() won won 't评估f(),因为第一个值是false),它们可能不会返回布尔值,但是表达式元素(例如,1 or 3将返回1)。

错误处理

这是一个让我烦恼的话题,在大多数语言中。我相信尝试捕获构造是古老的,只会使代码更难阅读。 GO使用元组(response, err)做得更好,但是IFS err == nil的数量很荒谬,对于返回条件下确定值的功能的烦恼。

我想测试我称之为包装的机制:

file := fs.open('invalid')?
if file! as err return err

每次您需要检查错误时,都可以通过添加一个符号来包装表达式。结果 - 错误或值 - 将被包裹到一个可能的对象中,只能通过解开它来检索。

使用后修复符号执行拆卸,该符号在使用时返回错误或错误,并将可能的对象转换为其真实内容 - 错误或值。

这是防止错误传播的唯一方法,并且与其他语法上的花式结构一起,我相信该代码将更加干净。

fn readConfig(filename) {
  file := fs.open(filename)?
  if file! return false

  data := json.load(file)?
  if data! return false

  res := validate(data)?
  if res! return false

  return data
}

我以为我可以强制某些函数默认情况下返回可能的对象,例如,此fs.open将定义为fn open(name)? { ... },但随后它将对用户隐藏此行为。

我很想看看这将如何用一种可以在编译器时间验证该表达式的类型语言来奏效,但是我相信动态语言中的静态分析也可以完成工作。

迭代器

让我们谈谈语言的恒星:迭代器。

迭代器是一个接口,可以访问和穿越对象集合的元素,该元素通常与列表和字符串关联。在SHT中,您可以迭代大多数原始类型,例如数字和布尔值,这将产生一个值:

sht迭代器具有一个可以手动调用的next函数,并将返回迭代类型,其中包含迭代的值或错误或完成的标志。但是,大多数情况下,这种行为是透明的。创建迭代器的最简单方法是声明发电机函数:

fn oneTwoThree {
  yield 1
  yield 2
  yield 3
}

iter := oneTwoThree()
iter.next() # Iteration<(1,)>
iter.next() # Iteration<(2,)>
iter.next() # Iteration<(3,)>
iter.next() # Iteration<Done>

用收益率语句函数默认情况下返回迭代器,而无需一开始就执行其身体,然后,对于每个呼叫,每个呼叫都将运行,直到找到功能的产量或末尾为止。请注意,通过这种行为,迭代器是处理元素的,默认情况下是懒惰的。

单独将迭代器范围范围范围划分到函数调用,因此发电机的多个调用将导致多个独立的迭代器。

可以通过使用“ ...”(默认情况下)在元组中通过“ ...”来扩散迭代器:

a := 'sht'
a... # ('s', 'h', 't')

请注意,分散的操作员迫使目标对象的迭代。

现在有趣的部分:迭代器可以用管道表达式:

data LinkedNode {
  val = 0
  prev = false
  next = false
}

head, tail := range(100)
| filter num: num % 2 == 0
| map num: LinkedNode { value = num }
| window(2) prv, nxt: { prv.next = nxt; nxt.prev = prv; }
| firstLast head, tail: head[0], tail[1]

让我们分解:

  • 第一行是返回迭代器的函数,该函数从0到100生成数字。
  • 第二行从管道开始,迫使先前的表达式返回迭代器。在内部,这意味着评估将在上一个对象上调用元函数。由于上一个对象已经是迭代器,因此它将仅返回自身。
  • 然后将范围迭代器作为第一个参数传递给过滤器函数。
  • 遵循过滤器函数是param[, param]*: <expression>表中的新功能声明,请注意,声明本身是可选的。
  • 过滤器本身返回下一个管道将使用的迭代器。
  • 窗口函数接收其他参数,在内部,它等效于window(prev_iterator, 2, (prv, nxt) => { ... })
  • 到最后,有一个firstLast函数,它将获得上一个迭代器和最后一个元素,迫使上一个迭代器完成。由于这些元素是由窗口函数生成的元组(prv, nxt),因此我们返回尾巴和最后一个元素的第一个元素,分别获得第一个和最后一个节点。

我的想法是,通过使用迭代器和管道表达式,用户被迫清楚,直接显示意图,避免抽象的每次申请水平(因此是由设计模式引起的间接意图)。

这就是我决定使用干净的语法进行管道的原因,而无需插入太多符号。

自定义数据类型

我事先尝试了很多选择,以查看该语言中适合的内容,而且说实话,什么都没有加紧。我相信最近语言中最有趣的想法来自静态语言,但没有真正适用于动态语言。所以我去了一个舒适的位置。

SHT自定义数据类型简单地定义了抽象数据类型(ADT),即对这些变量的一堆变量和操作。要定义一个新数据类型,您可以使用:

data MyADT {
  property = 'default value'

  fn say(this) {
    print(this.property)
  }
}

这是属性和函数的定义,因此您无法在数据定义中执行语句。属性的定义应带有默认值。由于我们没有零,所以我认为拥有明确的默认值很重要。

还注意,将在实例中起作用的函数应包含一个特殊的关键字this作为第一个参数,该参数告诉解释器此函数需要对象的引用,并且只能在该类型的实例上调用。自然,不包含此函数,因为第一个参数被视为静态函数,只能在类型本身中调用。

初始化

实例的初始化有点花哨:

data Vector {
  x = 0
  y = 0
  id = randomName()

  on new(this, x=0, y=0) {
    this.x = x
    this.y = y
  }
}

v1 := Vector(3, 2) {
    id: "My Vector"
}
v2 := Vector(3, 2)
v3 := Vector { id: 'Other vector' }

可以使用括号和/或括号来初始化数据类型。他们的行为略有不同。这是初始化矢量对象的初始化,如果它们可用,则第一件事是检查牙套中的值。这迫使数据在不评估属性定义中提供的默认值的情况下使用这些值,因此,randomName()函数将永远不会在v1v3中执行。但是,如果未在括号内声明该属性,则将使用默认表达式。括号调用之后的new meta函数已创建并初始化了实例。因此,您可以在内部添加其他初始化逻辑,例如数据库连接。

元编程

元编程类似于Python,您只需在数据定义内声明一个特殊功能,而解释器可以处理其他所有功能。我决定更改函数关键字,而不是使用特殊名称,以声明元功能,必须使用on而不是fn。这将有助于检查是否有任何无效的元功能声明。

数据类型可以实现下面描述的元函数,按照它们被挂接的表达式:

  • 呼叫:v1()
  • set:v1.x = 3
  • 获取:v1.x
  • 新:Vector()
  • 索引:v1[0]
  • iter:v1...
  • 管道:iterator | v1
  • 书:if v1 { ... }
  • 字符串:'Vector is:' .. v1
  • reter:在reple
  • 中使用
  • eq:v1 == v2
  • neq:v1 != v2
  • add:v1 + v2
  • 所有其他算术运算符

大多数可以在类型本身中实现(只需省略this参数),因此它们为我们提供了在类型系统工作方式上非常灵活的机会。但是,当您考虑迭代器以及它们如何在管道表达式中相互作用时,这些非常有用。这些元功能的另一个用法是创建代理,模拟和存根。

重用

这个话题是我对数据类型的最不关心的问题,因此我对此并不想出太多想法。一般的想法是在其内部复制另一种数据类型定义,而没有oo语言的儿童关系。

data Vector3 like Vector2 {
    z = 0
}

控制流

这里没有什么新鲜事物,但我相信值得注意的是,我添加的一些要点以使语言保持较少的冗长:

if <cond> return <exp>
if <cond> raise <exp>
if <cond> yield <exp>
if <cond> {
  <exp>
}

match <value> {
  <exp>: <exp>
}

for { <exp> }
for <cond> { <exp> }

pipe <iterator> as <var> { <exp> }

请注意,管路环是for的迭代器版本,类似于python的作品。我决定创建一个新的关键字,因为我找不到避免for含糊不清的好方法,例如,for var in iterator {}无法正常工作,因为in已经是元操作员关键字,并且适合for <cond> {}语句。

例子

愚蠢

for i in range(100) {
  match (i%3, i%5) {
    (0, 0): print('FizzBuzz')
    (0, _): print('Fizz')
    (_, 0): print('Bizz')
    (_, _): print(i)
  }
}

斐波那契

# Recursive
fn fib(n) {
  if n < 2 return n
  return fib(n-1) + fib(n-2)
}

# Generator
fn fib(n) {
  i := 0
  i, a, b := 0, 0, 1
  yield a
  yield b
  for i < n {
    a, b = b, a+b
    yield (i, b)
  }
}

# or use the default generator
math.fibonacci()
| takeWhile x: x < 10000

3和5的倍数

v := range(3, 1000)
| filter n: n%3 == 0 or n%5 == 0 
| sum

print(v)

结论?

这种SHT语言仍在开发中,完成了基本功能和内置模块后,我希望在我的日常自动化任务中使用一点,以更好地感觉自己的状态。我还将尝试解决诸如Euler挑战之类的编码挑战,以检查它。

如果您有兴趣,请检查github中的存储库。随时与我联系以获取任何问题或讨论:)