65.9K
CodeProject 正在变化。 阅读更多。
Home

ELENA编程语言简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.54/5 (62投票s)

2016年9月12日

MIT

33分钟阅读

viewsIcon

283035

downloadIcon

2184

ELENA是一种通用、面向对象、多态的语言,具有后期绑定

引言

编程语言是一项艰巨的开发和学习任务。所以遇到一门新语言,你可能会问:为什么还要有另一门编程语言?简短的回答是为了尝试一些不同的东西。为什么不把消息看作是普通的语言元素,可以进行不同的操作:加载、传递、修改、分派?我们能在不使用反射的情况下做到这一点吗?这就是我的动机。ELENA旨在促进动态、多态代码的编写,融合了动态和静态语言的元素。

ELENA是一种具有后期绑定的通用语言。它是多范式的,结合了函数式和面向对象编程的特性。它同时支持强类型和弱类型,运行时转换,原始类型的装箱和拆箱,直接使用外部库。提供了丰富的工具集来处理消息分派:多方法、消息限定、通用消息处理器。可以通过混入(mixins)和类型接口来模拟多重继承。内置脚本引擎允许将自定义脚本集成到您的应用程序中。支持独立应用程序和虚拟机客户端。

总而言之,让我们列举一些ELENA的功能

  • 免费开源(MIT许可)
  • 完整的源代码
  • GUI IDE 和调试器
  • 可选类型
  • 多重分派/多方法
  • 支持可变参数方法
  • 支持可yield方法
  • 闭包
  • Mixins
  • 类型接口/转换
  • 类/代码模板
  • 脚本引擎

支持的平台

ELENA **6.0** 支持 *x86 (Windows / Linux)*、*x86-64(Windows / Linux)*、*Aarch64(Linux)* 和 *PPC64le(Linux)*。

与其他语言的比较

主要问题是和什么语言比较?外面有成百上千种语言,其中几十种相当流行。而且大多数都是图灵完备的。所以这种比较总是相当主观的。但有一个专门的网站对此——Rosetta Code。你可以比较ELENA实现的近两百个任务与你可能想到的任何语言——http://rosettacode.org/wiki/Category:Elena。

我们来举一个——闭包/值捕获

在C#中

        var captor = (Func<int, Func<int>>)(number => () => number * number);
        var functions = Enumerable.Range(0, 10).Select(captor);
        foreach (var function in functions.Take(9))
        {
            Console.WriteLine(function());
        }

Clojure

(def funcs (map #(fn [] (* % %)) (range 11)))
(printf "%d\n%d\n" ((nth funcs 3)) ((nth funcs 4)))

在Smalltalk中

funcs := (1 to: 10) collect: [ :i | [ i * i ] ] .
(funcs at: 3) value displayNl .

在ELENA中

    var functions := Array.allocate(10).populate::(int i => {^ i * i} ); 
    functions.forEach::(func) { console.printLine(func()) }

那么,哪一个更好?最快的那个?这取决于编译器。最短的那个?不知道。这就像在问哪种语言更好,法语还是英语?

可移植性

它用C++和汇编编写。所以支持新的CPU相当容易,只需要移植汇编部分。

可写性和可读性

当前的语法类似于C风格的语法(尽管不完全兼容)。所以它具有相同的优点/缺点。一般来说,ELENA使用LL(1)语法。可以通过模板扩展语言的语法结构。例如,if,while,for——是用户定义的代码模板模式。该语言没有内置关键字。它们也是用户定义的。(因此,它可以相当容易地本地化)。
该语言默认使用UTF-8源文件。

它的优点和缺点是什么?

缺点:编译器可能被认为是半工业级的(与业余编译器相比相当先进,但仍需大量努力才能获得良好的代码性能),用户基数非常小(实际上不存在),代码库很小。

优点:混入,消息分派,多方法,集成到编译器脚本引擎,一个调试器,完整的Unicode支持(包括关键字/属性),模板(类/运算符)。

在哪里可以找到编译器和源代码的可执行文件?

ELENA开发社区

构建ELENA编译器

ELENA编译器完全用C++实现。为了编译,编译器不需要外部依赖,如Flex、Bison……您只需要Visual Studio 2017或更高版本。

单元测试需要googletest

Alex实现了自己的语法生成器、汇编器以及他需要的工具。

要构建编译器,您可以打开CMD,转到elena的根文件夹并键入

  • recompile60.bat

或者,要生成发行版文件

  • build\create_package_x86.bat elena-lang-6.2.0-win32

入门

在开始用100%面向对象语言编程之前,了解一门真正面向对象语言的基本概念是极其重要的。

基本OOP概念和术语

ELENA的基本概念是什么?

  • 对象
  • 字段
  • 参考文献
  • 方法
  • 消息
  • 继承
  • 接收者
  • 动态绑定

在OOP中,类定义了一组对该类型的所有对象都通用的属性和方法。它可能包含创建其实例的特殊方法,称为构造函数。

对象

对象是类的实例。通常,对象存储在动态内存中。对象有一个特殊情况——单例,它只存在一个实例。*请注意,在ELENA中,类本身就是一个对象,可以像任何其他对象一样使用。*

字段

字段,也称为实例变量,是在类中声明的变量。这些变量只能通过方法来读取,以分配或读取字段的内容。支持*静态*字段。

参考文献

在C++中,我们谈论对象的“指针”,而在C#中,我们谈论对象的“引用”。这两个概念基本相同,引用是通过直接指针实现的,而指针就是内存地址。在ELENA中,我们使用引用。

方法

方法就像函数,只是它应用于特定的对象。我们也说方法在对象上“调用”或“发送到”对象。相关的对象被称为“接收者”。每个方法都在接收对象上调用。在C++或C#中,接收者称为“this”,在ELENA中——称为“self”。

ELENA中的方法类似于C#和C++中的方法,在这些语言中它们被称为“成员函数”。方法可以接受参数,并始终返回一个结果(如果未提供结果,则返回“self”引用)。方法体是一系列可执行语句。方法就像其他语言一样,从表达式中调用。

消息

“方法”和“消息”之间有一个重要的区别。方法是一段代码,而消息是发送出去的东西。方法类似于函数。在这个类比中,发送消息类似于调用函数。调用方法的表达式称为“消息发送表达式”。

ELENA的术语清楚地区分了“消息”和“方法”。消息发送表达式将消息发送到对象。对象如何响应消息取决于对象的类。不同类的对象将以不同的方式响应同一消息,因为它们将调用不同的方法。通用方法可以接受具有指定签名(参数类型)的任何消息。

“Hello World!”应用程序

当我们学习或只是了解任何新的编程语言时,一个很好的起点总是著名的程序“Hello world!”。

为了创建我们的第一个程序,我们必须获取ELENA编译器,为此,您只需在此链接下载编译器:

下载并提取所有内容到您选择的文件夹后……

重要提示:您需要将“bin”文件夹添加到系统路径中。

bin文件夹添加到系统路径中后,这是生成可执行文件以访问elena运行时所必需的,例如,当异常发生时,该运行时会显示异常发生的类和方法。

让我们开始吧,首先,我们需要打开ELENA IDE,为此,您可以键入CMD“elide”或直接打开名为“elide”的可执行文件。

打开elide IDE后,我们将创建一个新项目,为此,只需转到菜单“文件->新建->项目”,当您单击项目菜单时,它将打开一个如下所示的对话框。

每个字段的含义

  • 类型:您将创建的应用程序的类型,例如GUI、控制台、在VM上运行的控制台应用程序……
  • 应用程序的命名空间
  • 关于未解析引用的警告复选框
  • 编译器的其他选项
  • 目标文件名:输出文件名
  • 输出路径:ELENA对象的输出
  • 启用或禁用调试器
  • 用于测试应用程序的命令行参数

对于本文,我们将使用这些特定的设置

单击“确定”后,我们需要创建一个新的源文件,为此,只需转到菜单“文件->源文件”或使用快捷键“Ctrl+N”。

现在,我们可以开始编码了……这是Hello World的代码。

public program()
{
    system'console.writeLine("Hello World!");
}

编写完此代码后,您需要将项目和源代码保存在某个目录中,然后只需构建它。

理解代码

public program()

在这里,我们声明了一个public函数(一个包含function消息的static class)。默认情况下,“program”是程序的入口点,但可以更改。

system'console.writeLine("Hello World!");

在这一行代码中,我们有一些非常有趣的信息需要关注

  1. system”:这是一个命名空间,命名空间本身可以包含由撇号分隔的子元素
  2. console”:这是一个类
  3. writeLine”:这是一个消息
  4. Hello World!”:这是消息的参数
  5. ;”:终止符

这对我们意味着什么?
这意味着我们向系统命名空间中控制台类的writeLine方法发送了一个消息,参数为“Hello World!”。

与其他语言(如C#、C++、Python)一样,我们可以添加一个特殊的关键字来“导入”一些命名空间,这样我们可以“缩减”代码。

因此,“import”关键字的hello world示例将如下所示:

import system;

public program()
{
    console.writeLine("Hello World!");
}

我们用这个程序遇到的唯一问题是我们无法读取输出……为了解决这个问题,我们只需要再使用一个方法,并且我们可以用两种方式使用这个方法。

First

import system;

public program()
{
    console.writeLine("Hello World!");
    console.readChar();
}

第二种

import system;

public program()
{
    console.writeLine("Hello World!").readChar()
}

最后一个分号可以省略。

第二种编写相同程序的方式有效,因为在ELENA中,每个方法都返回一个对象。

声明变量

变量是程序内存中(例如在函数堆栈中)的一个命名位置,其中包含一个值。在最简单的情况下,它是对象或null指针的引用——**nil**值。变量名可以包含任何UTF-8字符(特殊字符、空格除外;首字符不能是数字),并且区分大小写。没有保留关键字。不允许使用重复名称的变量(在同一作用域中声明)。

public program()
{
    var myFirstVariable := "Hello";
    var Δ := 1;
}

新的变量声明以*属性* **var**开头,绑定的值放在赋值运算符之后(*注意:ELENA中的赋值运算符和等价运算符不同*)。值也可以是表达式的结果。

声明和赋值可以分开

public program()
{
    var myFirstVariable;         // declaring a new variable myFirstVariable
    myFirstVariable := "Hello";  // assigning a value
    var Δ := 1;                  // declaring and assigning in the same statement
    Δ := Δ + 1;
}

如果变量在没有赋值的情况下声明,其值为**nil**。

变量类型在ELENA中是可选的。如果未显式声明类型,则变量是无类型的(或更准确地说,其类型为**system'Object**,即超类)。这意味着不进行类型检查,并且可以分配任何值。

    var v := "Hello";
    
    v := 3;

实际上,在许多情况下我们确实关心变量类型。因此,让我们创建一个强类型变量。必需的类型(类名或别名)应写在变量名前面。

    var string v := "Hello";

其中**string**是**system'String**类的别名。

**var**属性在这种情况下是可选的,我们可以简化代码。

    string v := "Hello";

变量类型限制了变量的可能值。与静态类型语言的最大区别在于,类型检查发生在**运行时**(尽管编译器会警告如果两个变量都是强类型的,则找不到转换例程)。因此,例如,以下代码可以编译,但会生成运行时错误。

public program()
{
    var o := 2;
    string v := o;
}

输出是:

system'IntNumber : Method #cast[0] not found
Call stack:
system'Exception#class.new[1]:exceptions.l(96)
system'MethodNotFoundException#class.new[1]:exceptions.l(1)
system'MethodNotFoundException#class.new[2]:exceptions.l(190)
system'$private'entry.#invoke[0]:win32_app.l(42)
mytest'program.#invoke[0]:test.l(6)
system'$private'entry.#invoke[0]:win32_app.l(37)
system'#startUp:win32_app.l(55)

如果声明了适当的转换方法,则值可以成功转换。

var o := 2;
real r := o;

在这种情况下,**system'IntNumber**类支持转换为**system'RealNumber**类型。

可以使用**auto**属性从其初始值设定项自动推断变量类型。

auto r := 1.2r;

变量类型通过赋值值——数字文字——**1.2r**来定义,即**system'RealNumber**(或**real**)。

控制流

ELENA支持丰富的控制流结构:分支、循环、异常处理。它们实现为代码模板,并且可以轻松扩展。分支和循环语句由布尔表达式控制。尽管分支运算符也可以处理非布尔结果。异常用于优雅地处理关键错误。异常可以包含堆栈跟踪以标识错误源。

布尔类型

**BoolValue**(别名**bool**)是一种布尔类型,只有两个可能的值:**true**和**false**。它是比较操作的典型结果。控制流语句需要布尔值(尽管也可以使用非布尔结果,前提是存在转换操作)。它支持典型的比较和逻辑操作。

console
    .printLine("true==false : ",true == false)
    .printLine("true!=false : ",true != false)
    .printLine("true and false : ",true && false)
    .printLine("true or false : ",true || false)
    .printLine("true xor false : ",true ^^ false);

结果是:

true==false : false
true!=false : true
true and false : false
true or false : true
true xor false : true

布尔反演通过**Inverted**属性实现。

console.printLine("not ",true,"=",true.Inverted)

结果是:

not true=false

布尔值可以直接用于分支操作。可以向其发送带有两个无参数函数的**if**消息。**true**值将执行第一个函数,**false**——第二个函数。

import extensions;

public program()
{
    var n := 2;
    
    (n == 2).if({ console.printLine("true!") },{ console.printLine("false!") });
}

结果是:

true!

还支持**iif**消息:true值将返回第一个参数,false——第二个参数。

var n := 2;

console.printLine((n != 2).iif("true", "false") )

输出如预期:

false

分支运算符

ELENA支持内置分支运算符**?**。它要求左操作数是布尔值。真部分参数紧跟在运算符后面。可选的假部分由感叹号引入。

import extensions;

public program()
{
    var n := 2;
    
    (n == 2) ? { console.printLine("n==2") };
    (n == 3) ? { console.printLine("n==3") } ! { console.printLine("n!=3") }
}

将打印以下结果:

n==2
n!=3

如果右操作数不是函数,运算符将返回真部分(如果左操作数为true)和假部分(否则)。

var n := 2;

console.printLine(n == 2 ? "n==2" : "n!=2");

结果类似:

n==2

分支语句

ELENA不为控制语句保留关键字。但有一组代码模板有助于使代码更具可读性。程序员可以自行扩展此列表。

让我们从许多人都熟悉的**if-else**开始。

var n := 2;
if (n == 2)
{
    console.printLine("n==2")
}
else
{
    console.printLine("n!=2")
}

结果是:

n==2

如果我们只需要真部分,可以省略else块。

if (n == 2)
{
    console.printLine("n==2")
}

或者,可以只写else部分。

ifnot (n == 2)
{
    console.printLine("n!=2")
}

如果代码块只包含单个语句,我们可以直接编写该语句——如果它是最后一个块。

if(n == 2)
   console.printLine("n==2");

条件类型转换

可以使用特殊的**if-is**语句来尝试类型转换对象。

    add(o)
    {
        if (o; is int n) {
           ^ self.add(n);
        };

        console.printLine("unsupported")
    }

如果变量**o**可以转换为整数,并且新变量n包含转换后的值,则将执行花括号内的代码。

循环语句

ELENA提供了几种类型的循环结构:for,while,until,do-until,do-while。

**FOR**循环用于指定迭代次数来执行循环体。它由初始化、条件和迭代表达式(以分号分隔)以及主循环体组成。

import extensions;

public program()
{
    for (var i := 0; i < 5; i++)
    {
        console.printLine(i)
    }
}

第一个表达式声明并初始化循环变量,第二个表达式在变量处于迭代范围之内时进行检查,第三个表达式在主循环体执行后增加循环变量。

结果将是。

0
1
2
3
4

类似地,如果循环体只包含单个语句,我们可以省略括号。

import extensions;

public program()
{
    for (var i := 0; i < 5; i++)
        console.printLine(i)
}

如果我们需要在检查条件之前执行迭代步骤,我们可以跳过最后一个表达式。在这种情况下,第一个表达式将在每一步同时初始化和迭代循环变量。条件(第二个表达式)的工作方式类似,循环将一直继续,直到遇到假值。结果是,迭代步骤保证至少执行一次。

import extensions;

public program()
{
    var sum := 0;
    for(var line := console.readLine(); line != emptyString)
    {
        sum += line.toInt()
    };
    
    console.printLine("The sum is ", sum)
}

在此示例中,我们从控制台读取数字,直到遇到空行,并打印总和。**readLine**方法从控制台读取下一行字符。**emptyString**是空字符串字面量常量。扩展方法**toInt**将字符串转换为整数。

输出如下:

1
3
5

The sum is 9

**WHILE**是一个经典的循环结构,它在条件为真时执行代码。让我们编写一个枚举循环。**枚举器**是特殊的*对象*,它们枚举集合,逐个返回成员。我们将枚举一个字符串。

import extensions;

public program()
{
    auto s := "♥♦♣♠";
    auto e := s.enumerator();
    while (e.next()) {
        console.printLine(e.get())
    }
}

结果将是。

♥
♦
♣
♠

相反的结构是**UNTIL**。它重复代码,直到条件为真。让我们重复代码,直到数字不为零。

import extensions;

public program()
{
    var n := 23;
    until(n == 0)
    {
       console.printLine(n);
     
       n /= 3
    }
}

输出将是:

23
7
2

很明显,这两种循环结构都是可互换的。选择可能取决于语义含义:是为了强调一个条件还是另一个条件。

如果我们想至少执行一次循环,无论条件如何,我们必须使用**DO-WHILE**或**DO-UNTIL**。让我们计算5的阶乘。

import extensions;

public program()
{
    int counter := 5;
    int factorial := 1;

    do {
        factorial *= counter;
        counter -= 1
    }
    while(counter > 0);
    
    console.printLine(factorial)
}

请注意,我们必须在**while**令牌后加上冒号。

结果如预期:

    120

可以使用特殊的**foreach**循环来轻松遍历枚举值。

import extensions;
import algorithms;

public program()
{
   var array := new int[] { 2,3,4 };

   foreach(auto item; in array.quickSort()) {
      console.writeLine(item);
   }
}

?. 和 \. 运算符

ELENA支持以下模板运算符:doIfNotNil (?., 类似于C#) 和 tryOrReturn (.\)。

doIfNotNil 仅当**左操作数**不为nil时才发送消息。

tryOrReturn 尝试向**左操作数**发送消息并返回操作的结果。如果对象不处理它——则返回nil

import system;
import extensions;

B
{
    test(string command)
    {
        console.printLine(command);
    }
}

C;

public program()
{
    var a := nil;
    var b := new B();
    var c := new C();
  
    a?.test("Fired a?.test");
    b?.test("Fired b?.test");
    
    a\.test("Fired a\.test");
    b\.test("Fired b\.test");
    c\.test("Fired c\.test");

    console.readLine();
}

输出是:

Fired b?.test
Fired a\.test

它也可以与函数调用一起使用。

public program()
{
    // declaring a nested function
    Func f1 := { console.writeLine("Fired 'f1'") };
    Func f2 := nil;
    
    f1?.();
    f2?.();
    
    console.readLine();
}

输出是:

Fired 'f1'

符号

符号是命名的表达式,可以用来使代码更具可读性。典型的符号是常量声明。尽管它们可以更多。符号用于声明全局单例实例。模块初始化代码是通过它们实现的。它们可以是私有的或公共的。

值/常量

符号的典型用例是可以在代码中进一步重用的值。

import extensions;
import extensions'math;

real PiOver2 = RealNumber.Pi / 2;

public program()
{
    console.printLine("sin(",PiOver2,")=", sin(PiOver2))
}

在这里,我们声明了一个私有的强类型符号PiOver2。可以省略类型。在这种情况下,它将是一个弱类型值。结果是:

sin(1.570796326795)=1.0

如果符号应该是公共的(可以在模块外部访问),则应在类型和名称之前加上**public**属性。

如果符号的值可以在编译时计算,我们可以声明一个常量符号。

import extensions;
import extensions'math;

public const int N = 3;

public program()
{
    for (int i := 0; i < N; i++)
    {
        console.printLine(i,"^2=", sqr(i))
    }
}

输出是:

0^2=0
1^2=1
2^2=4

静态符号

普通符号在每次使用时都会被计算。静态符号是一种特殊情况,它只在第一次调用时初始化(所谓的延迟加载),并且其值在每次后续调用时都会被重复使用。这是实现单例(如果它不是无状态的)的首选方式。

import extensions;
import extensions'math;

static inputedNumber = console.print("Enter the number:").readLine().toInt();

public program()
{
    console.printLine(inputedNumber,"^3 = ", power(inputedNumber, 3))
}

在此示例中,该符号仅被计算一次,并且输入的(值)被保留,因此数字应该只输入一次。

Enter the number:4
4^3 = 64

预加载符号

预加载符号是一种特殊符号,如果程序中使用命名空间成员,则在程序启动时会自动对其进行求值。因此,它可以用于模块初始化。

import extensions;

preloaded onModuleUse = console.printLine("Starting");

public program()
{
    console.printLine("Hello World")
}

输出是:

Starting
Hello World

Types

在编程中,数据类型(或简称类型)定义了数据的角色:数字、文本等。在面向对象编程中,这个概念已经扩展到了类,类封装了数据及其操作。数据存储在字段中,操作通过方法完成。在ELENA中,类型和类在大多数情况下含义相同,并且可以互换使用而不丢失含义(除了原始类型)。类基于继承形成层次结构。这意味着每个类(除了超类——**system'Object**)都有一个父类和一个父类。

编程语言可以具有静态或动态类型系统。在OOP中,类可以被制成多态的,这解决了静态类型问题的一部分。动态语言在运行时解析类型。这会影响它们的性能。现代编程语言因此试图结合这些方法。在ELENA中,类型是动态的,并在运行时解析。从这个意义上说,您可以编写程序而无需显式指定类型。但在大多数情况下,应指定类型以使代码更具可读性并提高其性能。

在动态语言中,调用方法的*操作*通常称为发送消息。类通过调用适当的方法(具有相同的名称和签名)来响应消息。在正常情况下,这是通过在方法表中搜索适当的处理程序来完成的。这需要时间。为了解决这个问题,ELENA允许声明**sealed**或**closed**类。密封类的*方法*可以在编译时解析并直接调用。关闭的类(或接口)的方法可以通过方法表在编译时解析。但密封类不能被继承。接口可以被继承,但不能声明新方法(私有方法除外)。

在ELENA中,类型转换是通过发送特殊方法——转换处理程序——来实现的。任何类都可以转换为另一个类,只要它处理该消息。否则,将引发异常。

原始类型

在大多数情况下,类和类型意味着相同。唯一的例外是原始类型。原始类型是内置的,只支持预定义的*操作*。它们是经典数据类型,因为它们是纯数据。ELENA支持以下类型:

类型 大小 描述
__float 8 64位浮点数
__int 1 8位整数
__int 2 16位整数
__int 4 32位整数
__int 8 64位整数
__raw   原始数据
__ptr 4 32位指针
__mssg 4 消息引用
__mssg 8 扩展引用
__subj 4 消息名称引用
__symbol 4 符号引用
__string   数组

原始类型可以通过适当的包装器在程序中使用。每次在代码中使用原始类型(原始操作除外)时,它都会被装箱到其包装器中。操作后,它会再次被拆箱。由于性能问题,不对原始操作进行任何验证,因此由程序员(或包装器类)正确处理。

撇开原始类型不谈,每个程序对象都是类的实例。类是封装数据(字段)及其操作(方法)的结构。类可以指定构造函数来创建对象,并指定转换例程来将其从另一个对象转换。

声明和使用类对于熟悉C风格面向对象语言的任何人来说都很简单。

import extensions;

// declaring a class named A
class A
{
    // declaring a field named a
    field a;
    
    // declaring a default constructor
    constructor()
    {
        a := "Some value"
    }
    
    // declaring a method named printMe
    method printMe()
    {
        console.printLine(a)
    }
}

public program()
{
    // creating an instance of A class
    var o := new A();
    
    // calling a method 
    o.printMe()
}

输出是:

Some value

关键字**class**指定一个普通类。类体包含在花括号中。类名紧跟在类体之前声明。字段可以在类体内的任何位置声明。它可以以**field**属性开头。隐式构造函数名为**constructor**。方法可以用**method**关键字声明。代码可以放在花括号内。

类形成层次结构。除了超类之外,每个类都有一个父类。如果未指定父类,则该类继承**system'Object**(超类)。子类可以覆盖其父类的任何非密封方法。子类实例同时也是父类型的实例(所谓的is-a关系)。因此,我们可以将子类分配给父类型变量。但将被覆盖的方法将被使用(多态代码)。在弱类型(或无类型)的情况下,这也是如此(我们只假设所有变量都是超类型的)。

import  extensions;

class Parent
{
    field f;
    
    constructor()
    {
        f := "some value"
    }
    
    printMe()
    {
        console.printLine("parent:",f)
    }
}

class Child : Parent
{
    printMe()
    {
        console.printLine("child:",f)
    }
}

public program()
{
    Parent p := new Parent();
    Child c := new Child();
    
    p.printMe();
    c.printMe()
}

输出将是:

parent:some value
child:some value

父子句应跟在类名之后,并用冒号引入。要覆盖方法,我们只需使用相同的名称和签名再次声明它。

在ELENA中,类型(或类)可以直接在代码中像任何其他符号一样使用。唯一的区别是我们不能调用默认构造函数。应该使用命名构造函数代替。*注意:默认构造函数将自动调用。*

import extensions;

// declaring a class named A 
class A
{
    field a;
    
    // default constructor
    constructor()
    {
        a := "a"
    }
    
    // explicit constructor named new
    constructor new()
    {
        // default constructor is called automatically
    }
}

// declaring a class named B
class B
{
    field b;
    
    // default constructor
    constructor()
    {
        b := 2
    }
    
    // explicit constructor named new
    constructor new()
    {
        // default constructor is called automatically
    }
}

// factory function
factory(class)
    // sending a message - name and returning a result of the operation 
    = class.new();

public program()
{
    // creating objects using a class directly
    var a := factory(A);
    var b := factory(B);
    
    // printing the object types
    console
        .printLine(a)
        .printLine(b);
}

输出是:

mylib'$private'A
mylib'$private'B

默认情况下,类被声明为私有。这意味着它不能在命名空间外部访问。对于库,我们希望重用它。所以我们必须提供一个公共属性。

public class B
{
}

现在,类可以通过引用在库外部或内部访问。

public program()
{
    var b := new mylib'B()
}

引用(或完整名称)由一个命名空间(其本身可以包含由撇号分隔的子命名空间)和一个由撇号分隔的实际名称组成。

抽象类

抽象类是一种特殊类,用作创建符合其协议或其支持的操作集的特定类的基础。抽象类不能直接实例化。它与接口不同,子类可以扩展其功能(声明新方法)。抽象类可以包含抽象方法和普通方法。抽象方法必须有一个空体(分号)。

import extensions;

abstract class Bike
{  
    abstract run(); 
}
  
class Honda4 : Bike
{  
    // overriding an abstract method
    run()
    {
        console.printLine("running safely")
    }
}    

public program()
{
    Bike obj := new Honda4();
    obj.run()
}        

输出将是:

running safely

基于抽象类的类必须实现所有父抽象方法。如果子类添加了新的抽象方法或仅实现了部分父抽象方法,它也应被声明为抽象。

abstract class BikeWithTrolley : Bike
{
    abstract pack();
}

接口

接口是抽象类的一种特殊情况。出于性能原因,不能为接口子类声明新方法(私有方法除外)。因此,接口方法可以半直接调用(通过方法表)。接口方法可以是抽象的和普通的(就像在抽象类中一样)。

interface IObserver
{
    // declaring an interface abstract method
    abstract notify();
}

虽然我们可以直接继承接口。

class Oberver : IObserver
{
    notify() 
    {
        console.printLine("I'm notified")
    }
}

但这是不切实际的,因为我们无法扩展类功能。相反,我们可以使用一个名为**interface**的模板。

import extensions;

// declaring an interface
interface IObserver
{
    // declaring an interface method to be implemented
    abstract notify();
}

// creating a class supporting an interface
class MyClass : interface<IObserver>
{
    // implementing an interface method
    notify() 
    {
        console.printLine("I'm notified")
    }
    
    // implementing some other functionality
    doSomework()
    {
        console.printLine("I'm doing some work")
    }
}

// simple routine to invoke the interface
sendNotification(IObserver observer)
{
    observer.notify()
}

public program()
{
    // creating an intance of MyClass
    auto myObject := new MyClass();
    
    // do some work
    myObject.doSomework();
    
    // passing the interface implementation to the function by 
    // explicit typecasting the object
    sendNotification(myObject :as IObserver);
}        

输出是:

I'm doing some work
I'm notified

类可以实现多个接口(因此我们可以绕过单重继承问题)。

import extensions;

// declaring an interface
interface IObserver
{
    // declaring an interface method to be implemented
    abstract notify();
}

// declaring the second interface
interface IWork
{
    abstract doSomework();
}

// creating a class supporting both interfaces
class MyClass : interface<IObserver>, interface<IWork>
{
    // implementing an interface method
    notify() 
    {
        console.printLine("I'm notified")
    }
    
    // implementing the second interface
    doSomework()
    {
        console.printLine("I'm doing some work")
    }
}

// simple routine to invoke the interface
sendNotification(IObserver observer)
{
    observer.notify()
}

public program()
{
    // creating an intance of MyClass 
    auto myObject := new MyClass();
    
    // implicitly typecast it to the second interface
    IWork work := myObject;
    
    // use the second interface
    work.doSomework();
    
    // passing the interface implementation to the function by 
    // explicit typecasting the object
    sendNotification(myObject :as IObserver);    
}

结果相同:

I'm doing some work
I'm notified

单例

单例是一种特殊的类,无法初始化(不允许构造函数),并且只有一个实例。单例大多是无状态的(尽管它们可以有静态字段)。它们总是被密封的。

单例类用**singleton**属性声明。它可以通过类引用直接引用。

import extensions;

// declaring a singleton
public singleton StringHelpers
{
    // Gets the first character of a string.
    char first(string str)
    {
        ^ str[0]
    }
}

public program()
{
    var str := "My string";
    
    // calling a singleton method
    console.printLine("Calling StringHelpers.First(""",str,""")=", 
         StringHelpers.first(str))
}

输出将是:

Calling StringHelpers.First("My string")=M

结构体

结构体是一种特殊的类,它们存储在原地,而不是存储在内存堆中的引用。结构体是密封的(意味着它们不能被继承)。它们主要包含小数据值(如数字、处理程序、指针)。所有原始数据处理程序都是结构体。

结构体字段可以是原始类型,也可以是另一个结构体。不允许引用类型。因此,所有字段都应该是强类型的。

在大多数情况下,结构体的使用都相当直接。

import extensions;

// declaring a struct
struct Record
{
   // declaring struct fields
   int x;
   int y;
   
   // declaring struct constructor
   constructor(int x, int y)
   {
       // using this prefix to distinguish a class member from the local one
       this x := x;
       this y := y
   }
   
   printMe()
   {
       console.printLine("Record(",x,",",y,")");
   }
}    

public program()
{
    // creating a struct
    auto r := new Record(2, 4);
    
    // invoking struct method
    r.printMe()
}

结果将是。

Record(2,4)

请注意,结构体是就地存储的。这意味着在我们的示例中,对象是在方法堆栈中声明的。每次它被用作弱类型时,它都会被装箱,并在操作后被拆箱。为了提高性能,我们可以将结构体声明为常量,以避免拆箱操作。例如,所有数字都是常量结构体。

// declaring a constant struct
public const struct IntNumber : IntBaseNumber
{
    // a field is a primitive 32-bit integer type   
    embeddable __int theValue[4];
...

字符串

字符串是一种特殊类型的类(结构体和非结构体类),用于包含数组。因此,类的长度是可变的。不允许使用默认构造函数(无参数)。

public const struct String : BaseValue
{
    __string byte[] theArray;

    constructor allocate(int size)
        = new byte[](size + 1);

它只能包含一个标记为**__string**的字段。

扩展

扩展是特殊的无状态类,用于声明扩展方法。扩展方法允许您在不创建新派生类型、重新编译或修改原始类型的情况下扩展原始类功能。在扩展类中声明的每个方法都是一个扩展方法。

在正常情况下,扩展类从不直接使用(混入除外)。要在其他模块中使用,它们应声明为公共的。要开始使用扩展,只需在与其将使用的代码相同的命名空间中声明它即可。如果扩展在另一个模块中声明,则应使用**import**语句将该模块包含到代码中。

import extensions;

// declaring an extension
public extension MyExtension
{
    // every member of the extension is an extension method
    printMe()
    {
        console.printLine("I'm printing ", self);
    }
}

public program()
{
    // invoking an extension method for various types
    2.printMe();
    "abc".printMe();
    2.3r.printMe()
}

结果是:

I'm printing 2
I'm printing abc
I'm printing 2.3

扩展方法在ELENA的许多地方都有使用。例如,**print**和**printLine**是扩展的可变参数方法。如果类已经有一个同名的方法,它将被使用而不是扩展方法。例如,如果我们用一个包含**printMe**方法的新类来扩展我们之前的示例。

MyClass
{
    printMe()
    {
        console.printLine("I'm printing myself");
    }
}

public program()
{
    auto o := new MyClass();
    o.printMe();
}

将调用正确的方法。

I'm printing myself

但是,如果对象是弱类型的,将调用扩展。

public program()
{
    var o := new MyClass();
    o.printMe();
}

输出将是:

I'm printing mytest'$private'MyClass

因此,不混合使用扩展和普通方法名称是一个好习惯。

扩展可以是弱的,这意味着它们可以扩展任何对象(或**system'Object**类的实例)。但我们总是可以指定确切的扩展目标。

// declaring an extension of MyClass
public extension MyStrongExtension : MyClass
{
    printMe()
    {
        console.printLine("I'm printing MyClass");
    }
}

MyClass
{
}

public program()
{
    auto o := new MyClass();
    o.printMe();
}

输出将是:

I'm printing MyClass

如果扩展目标不同,则可以具有多个同名的扩展方法。

扩展可以在编译和运行时进行解析。编译器尝试直接解析所有扩展。但如果存在多个相似的扩展,它将生成一个运行时分派器。

// declaring classes
A;
B;

// declaring several strong-typed extensions with the same name
extension AOp : A
{
    // extending an instance of A 
    whoAmI()
    {
        console.printLine("I'm instance of A")
    }
}

extension BOp : B
{
    // extending an instance of B 
    whoAmI()
    {
        console.printLine("I'm instance of B")
    }
}

public program()
{
    // declaring weak-typed variables
    var a := new A();
    var b := new B();

    // resolving an extension in compile-time is not possible
    // so the run-time dispatcher will be used
    a.whoAmI();
    b.whoAmI();
}

输出将是:

I'm instance of A
I'm instance of B

密封/关闭类

**Sealed**和**closed**属性用于提高与类的*操作*的性能。密封类不能被继承。因此,编译器可以为该类生成直接方法调用(当然,当类型已知时)。所有结构体和单例默认都是密封的。声明一个密封类相当简单。

sealed class MySealedClass
{
}

关闭的类可以被继承,但不能声明新方法(私有方法或新字段)。所有接口都是关闭的。当类型被关闭时,编译器可以使用方法表来解析方法调用。

closed class MyBaseClass
{
    // declaring a "virtual" method
    myMethod() {}
}

class MyChileClass : MyBaseClass
{
    // overriding a method
    myMethod() {}
}

基本类型转换

让我们学习如何将基本数据类型(intlongrealstring)相互转换。我们将使用extensions模块中声明的扩展方法。

让我们将基本类型的数字转换为**string**。

import extensions;

public program()
{
    int n := 20;
    real r := 3.4r;
    long l := 5000000000000l;
    
    console.writeLine("n=" + n.toString());
    console.writeLine("r=" + r.toString());
    console.writeLine("l=" + l.toString());
}

输出是:

n=20
r=3.4
l=5000000000000

我们可以为整数使用不同的基数。

import extensions;
                                                  
public program()
{
    int n := 20;
    long l := 5000000000000l;

    console.writeLine("n=0o" + n.toString(8));
    console.writeLine("l=0x" + l.toString(16));
}

输出是:

n=0o24
l=0x48C27395000

反过来也是可能的——使用toInt / toLong / toReal扩展方法。

import extensions;

public program()
{
    int n := "20".toInt();                 // = 20
    long l := "5000000000000".toLong();    // = 5000000000000l
    real r := "2.3".toReal();              // = 2.3r
}

也可以使用自定义基数。

import extensions;

public program()
{
    int n := "24".toInt(8);              // = 20
    long l := "48C27395000".toLong(16);  // = 5000000000000l
}

数值类型之间的*操作*也是可能的(只要它们兼容)。

import extensions;

public program()
{
    int n := 200;
    
    long l := n.toLong();    // = 200l
    real r1 := n.toReal();   // = 200.0r
    real r2 := l.toReal();   // = 200.0r
}

我们可以跳过显式转换。

import extensions;                                     :

public program()
{
    int n := 200;
    
    long l := n;       // = 200l
    real r1 := n;      // = 200.0r
    real r2 := l;      // = 200.0r
}

当实数没有小数部分时,可以进行从实数到整数的转换。

import extensions;

public program()
{
    real r := 200.0r;
    
    long l := r.toLong();     // = 200l
    int n := r.toInt();       // = 200
}

我们可以通过使用Integer扩展方法忽略小数部分。

import extensions;

public program()
{
    real r := 200.2r;
    
    real r2 := r.Integer;         // = 200.0r
    long l := r.Integer.toLong(); // = 200l
    int n := r.Integer.toInt();   // = 200
}

异常处理

异常处理旨在处理程序执行期间的意外情况。对于动态语言,典型的意外情况是向不处理该消息的对象发送消息(未声明适当的方法)。在这种情况下,将引发**MethodNotFound**异常。其他值得注意的例子是:除零、nil引用和内存不足异常。该语言提供了几种代码模板来处理这种情况。

ELENA中的典型异常继承自**system'Exception**基类。名为**raise()**的方法被声明并用于在代码中引发异常。**system**库中有几种异常:OutOfRangeException、InvalidArgumentException、InvalidOperationException、MethodNotFoundException、NotSupportedException、AbortException、CriticalException等。典型异常包含错误消息和指向异常创建时刻的调用堆栈。

在ELENA中引发异常非常简单。

divide(l,r)
{
    if(r == 0)
    {
        InvalidArgumentException.raise()
    }
    else
    {
        ^ l / r
    }
}

为了处理它,我们必须编写**TRY-CATCH**或**TRY-FINALLY**结构。

public program()
{
    console.print("Enter the first number:");
    var l := console.readLine().toInt();
    
    console.print("Enter the second number:");
    var r := console.readLine().toInt();

    try
    {
        console.printLine("The result is ", divide(l,r))
    }
    catch(InvalidArgumentException e)
    {
        console.printLine("The second argument cannot be a zero")
    };
}

输出将是:

Enter the first number:2
Enter the second number:0
The second argument cannot be a zero

**catch**令牌后声明的匿名函数将在引发的异常与函数参数列表匹配时调用。如果我们想处理任何标准异常,我们可以使用基类异常。

    catch(Exception e)
    {
        console.printLine("An operation error")
    };

可以在嵌套类中声明多个异常处理程序。

catch::
{
    function(InvalidArgumentException e)
    {
       console.printLine("The second argument cannot be a zero")
    }
    
    function(Exception e)
    {
        console.printLine("An operation error")
    }
}

在我们的例子中,两个处理程序都匹配InvalidArgumentException异常(因为InvalidArgumentException是Exception的子类),但将引发适当的异常处理程序,因为它描述得更早。

如果我们想在引发异常或代码正确工作时始终执行代码,我们必须使用**TRY-FINALLY-CATCH**结构。典型用例是资源释放。

import extensions;

public program()
{
    console.printLine("Try");
    try
    {
        var o := new Object();
        o.fail();
    }
    finally
    {
        console.printLine("Finally");
    }
    catch(Exception e)
    {
        console.printLine("Error!");
    }
}

输出将是:

Try
Finally
Error!

如果注释掉第二行,输出将是:

Try
Finally

如果我们想在操作后执行代码,无论是否引发异常,都可以跳过catch部分。

    console.printLine("Try");
    try
    {
        var o := new Object();
        o.fail();
    }
    finally
    {
        console.printLine("Finally");
    }

结果是:

Try
Finally
system'Object : Method fail[0] not found
Call stack:
system'Exception#class.new[1]:exceptions.l(96)
system'MethodNotFoundException#class.new[1]:exceptions.l(1)
system'MethodNotFoundException#class.new[2]:exceptions.l(190)
mytest'program.#invoke[0]:test.l(14)
mytest'program.#invoke[0]:test.l(9)
system'$private'entry.#invoke[0]:win32_app.l(37)
system'#startUp:win32_app.l(55)

using语句

当我们要确保对象始终正确关闭时,可以使用using模式。

import extensions;

class A
{
    constructor()
    {
        console.printLine("opening");
    }
    
    do()
    {
        console.printLine("doing");
    }
    
    close()
    {
        console.printLine("closing");
    }
}

public program()
{
    using(var a := new A())
    {
        a.do();
    }
}

结果是:

opening
doing
closing

文件操作

让我们逐行读取文本文件。

import system'io;
import extensions;

public program()    
{
    // getting a file name from the command line argument
    var fileName := program_arguments[1];

    // opening a text file reader
    using(auto reader := File.assign(fileName).textreader())
    {
        // repeating until all the lines are read 
        while (reader.Available)
        {
            // read the line
            string line := reader.readLine();
            
            // print the line
            console.printLine(line);            
        };
    }    
}

让我们写入文件。

import system'io;
import extensions;

public program()    
{
    // getting a file name from the command line argument
    var fileName := program_arguments[1];

    // opening a text file writer
    using(auto writer := File.assign(fileName).textwriter())
    {
        // repeating until the empty line is entered
        for(string line := console.readLine(), line != emptyString, line := console.readLine())
        {
            // save to the file 
            writer.writeLine(line)
        }
    }    
}

如果指定名称的文件存在,将被覆盖。

如果我们要追加到现有文件,那么我们应该使用logger()方法,例如:

using(auto writer := File.assign(fileName).logger())

让我们读取二进制文件。

import system'io;
import extensions;
import system'collections;

public program()    
{
    // getting a file name from the command line argument
    var fileName := program_arguments[1];

    // creating a list to hold the read data
    List<byte> dump := new List<byte>();

    // opening a binary file reader
    using(auto reader := BinaryFileReader.new(fileName))
    {
        // repeating until all the file is read 
        while (reader.Available)
        {
            // read the current byte into a variable b
            // note the variable is declared just-in-place
            reader.read(ref byte b);
            // add the read byte to the dump 
            dump.append(b);
        };
    };
    
    // print the file content as a list of bytes
    console.printLine(dump.asEnumerable());
}

最后,让我们搜索文件和目录。

import system'io;
import extensions;
   
public program()
{
    // specifying the target directory
    var dir := Directory.assign(".");
    
    var files := dir.getFiles();
    var dirs := dir.getDirectories();

    console.printLine("Directory ", dir);
    console.printLine("files:", files.asEnumerable());
    console.printLine("directories:", dirs.asEnumerable());
}

惰性表达式

惰性表达式是在访问时才计算的表达式。

import extensions;

public program()
{
    var expr := (lazy:console.write("Reading a number:").readLine().toInt());
    
    // the expression is invoked only when it is used
    var sum := expr + expr;
    
    console.printLine("the result is ", sum);
}

输出是:

Reading a number:2
Reading a number:3
the result is 5

使用可枚举对象

ELENA支持几种不同类型的可枚举对象:数组、列表、字典、范围等。

为了被用作可枚举对象,对象应该处理enumerator消息。

让我们看一个简单的例子。

import extensions;

public program()
{
    var list := new{1,2.3r,"String",3l};
    
    var it := list.enumerator();
    while (it.next())
    {
        console.printLine(it.get())
    }
}

在输出中,将打印集合的每个成员。

1
2.3
String
3

在此代码中,我们声明了一个static数组——list。我们向它发送enumerator消息,并将返回的enumerator分配给变量it。然后,我们重复代码,直到next消息返回false值。get消息返回当前的枚举成员。

相同的模式可以应用于任何可枚举对象。

import extensions;
import system'routines;

public program()
{
    var range := new Range(1, 9);
    
    var it := range.enumerator();
    while (it.next())
    {
        console.printLine(it.get())
    }
}

在这里,我们生成并打印一个从110的自然数范围。结果是:

1
2
3
4
5
6
7
8
9

与C#类似,有许多扩展方法可用于搜索、计数、过滤等。

让我们从一个简单的开始——forEach——为每个枚举成员执行代码。上面的代码可以使用forEach扩展方法(在system'routines模块中声明)和闭包重写。

import extensions;
import system'routines;

public program()
{
   new Range(1, 9).forEach:(item){ console.printLine(item) }
}

其中(item){ <...> }是一个带有单个参数的通用闭包。

我们可以使用现有的闭包extensions'routines'printingLn进一步简化我们的代码。

import extensions;
import system'routines;
import extensions'routines;

public program()
{        
    new object[]{1,2.3r,"String",3l}.forEach:printingLn
}

在这两种情况下,输出都将与我们的前两个示例类似。

我们可以将多个扩展方法串联起来。例如,filterBy用于根据参数输出来过滤枚举。只有当filter函数返回true时,成员才会被传递。它可能与其他扩展方法(如forEach)结合使用。

import system'routines;
import extensions'routines;
import system'math;

public program()
{
    new Range(1, 9).filterBy:(i => i.mod:2 == 0).forEach:printingLn
}

filterBy将只返回偶数。输出将是:

2
4
6
8

请注意,(item => <...> )是lambda闭包,是(item){ ^<...> }的简写形式。

summarize扩展用于总结集合的所有成员。

import extensions;
import system'routines;
import extensions'routines;
import system'math;

public program()
{
    console.printLine(new Range(1, 9).filterBy:(i => i.mod:2 == 0).summarize())
}

结果将是前10个偶数自然数的总和。

20

使用toArray扩展,我们可以将枚举保存为数组。

import extensions;
import system'routines;
import extensions'routines;
import system'math;

public program()
{
    var evens := new Range(1, 9).filterBy:(i => i.mod:2 == 0).toArray();

    console.printLine("sum(",evens,")=", evens.summarize())
}

toArray会将枚举成员收集到数组中。输出是:

sum(2,4,6,8)=20

我们可以使用top扩展来限制我们的输出。

import system'routines;
import extensions'routines;
import system'math;

public program()
{
    new Range(1,100).filterBy:(i => i.mod:2 == 0).top:10.forEach:printingLn
}

结果将是前10个偶数。

2
4
6
8
10
12
14
16
18
20

我们可以使用*zip:by*扩展将多个可枚举对象组合成一个集合。

import extensions;
import system'routines;
import extensions'routines;

symbol list = new string[]{"a","b","c","d"};

public program()
{
    list
       .zipBy(new Range(1, list.Length), (ch,i => i.toPrintable() + " - " + ch.toPrintable()))
       .forEach:printingLn
}

输出是:

1 - a
2 - b
3 - c
4 - d

其中(ch,i => <...>)是一个带有两个参数的lambda闭包。

selectBy扩展可用于基于前一个集合生成新集合。orderBy扩展将对集合进行排序。

import extensions;
import system'routines;
import extensions'routines;

public program()
{
    var list := new Range(1,5).selectBy::(n => randomGenerator.nextInt(100)).toArray();
    
    console.printLine("sort(",list,")=",list.orderBy::(p,n => p < n))
}

结果将是随机生成的数字列表,按升序排列。

sort(50,94,40,78,93)=40,50,78,93,94

groupBy可用于将枚举成员分组为子集合。

import extensions;
import system'routines;
import extensions'routines;

public program()
{
    var list := new Range(1,20).selectBy::(n => randomGenerator.nextInt(10)).toArray();
    
    list
        .groupBy::(x => x)
        .selectBy::(sub_list => 
                      sub_list.Key.toPrintable() + ":" 
                      + sub_list.countMembers().toPrintable() + " times")
        .orderBy(ifOrdered)
        .forEach(printingLn)
}

代码将计算随机数字遇到的次数。

0:2 times
1:4 times
2:6 times
3:2 times
4:2 times
5:4 times
6:10 times
7:6 times
8:2 times
9:2 times

枚举类型

枚举类型是一种特殊类型的对象,它只能包含枚举值。

虽然ELENA不直接支持此类型(作为一个纯粹的面向对象语言,它只支持类)。但从ELENA 6.2.1开始,将可以使用特殊的后缀模板:enum。

声明非常简单,我们声明类型并在括号中列出可能的值。

    public const struct Color : enum<int>(Red = 1,Green = 2,Blue = 3);

在代码中使用它非常简单。

    Color red := Color.Red;
    Color green := Color.Green;

可以使用**toPrintable**方法转换为字符串。

    console.printLine(red.toPrintable());
    console.printLine(green.toPrintable());

输出将是:

    Red
    Green

可以使用**enumerator**方法或**foreach**操作列出所有可能的值。

    foreach(Color c; in Color)
    {
       console.writeLine(c);
    }

结果将是。

    Red
    Green
    Blue

动态分派、通用处理程序和混入

让我们从多方法开始。我们可以声明多个具有相同名称但签名不同的方法。也可以声明显式多方法分派器。

class MyClass
{
    // accepts the integer
    testMe(int n)
    {
        console.writeLine("It is a number")
    }

    // accepts the string
    testMe(string s)
    {
        console.writeLine("It is a string")
    }

    // default handler
    testMe(o)
    {
        console.writeLine("Unsupported parameter")
    }
}

public program()
{
    object o := new MyClass();

    o.testMe(2);
    o.testMe("s");
    o.testMe(3l)
}

输出是:

It is a number
It is a literal
Unsupported parameter

在某些情况下,也可以做相反的事情,我们可以声明通用处理程序,它将接受任何传入的消息。

import extensions;
 
class Example
{
    generic()
    {
        // __received is an built-in variable containing the incoming message name
        console.printLine(__received," was invoked")
    }
 
    generic(x)
    {
        console.printLine(__received,"(",x,") was invoked")
    }
 
    generic(x,y)
    {
        console.printLine(__received,"(",x,",",y,") was invoked")
    }
}
 
public program()
{
    var o := new Example();
 
    o.foo();
    o.bar(1);
    o.someMethod(1,2)
}

输出

foo was invoked
bar(1) was invoked
someMethod(1,2) was invoked

我们可以声明一个自定义分派器,它将所有未映射的传入消息重定向到另一个对象,从而有效地覆盖它(某种动态变异/代码注入)。

import extensions;

class Extender
{
    object theObject;
    
    // the injected property
    prop object Foo;
    
    constructor extend(o)
    {
        theObject := o
    }
    
    // redirect Object.Printable method to foo one
    string toPrintable() => theObject;

    // custom dispatcher
    dispatch() => theObject;
}

public program()
{
    var o := 234;
  
    // adding a field
    o := Extender.extend(o);

    // setting a field value
    o.Foo := "bar";

    console.printLine(o,".foo=",o.Foo)
}

输出是:

234.foo=bar

消息可以动态分派。

class MyClass
{
    eval()
    {
       console.writeLine("eval method")
    }                                                    

    state0()
    {
       console.writeLine("state0 method")
    }
}  

public program()
{
   var o := new MyClass();

   var mssg := mssgconst state0[1];  // a message literal

   mssg(o)                           // dynamically dispatching the message
}

输出是:

eval method
state0 method

虽然ELENA不支持多重继承,但使用自定义分派器,我们可以模拟它。

singleton CameraFeature
{
    cameraMsg
        = "camera";
}
 
class MobilePhone
{
    mobileMsg
        = "phone";
}
 
class CameraPhone : MobilePhone
{
    dispatch() => CameraFeature;
}
 
public program()
{
   var cp := new CameraPhone();
 
   console.writeLine(cp.cameraMsg);
   console.writeLine(cp.mobileMsg)
}

输出是:

camera
phone

从外部程序使用ELENA代码

语言编译器支持两种类型的输出:独立和VM客户端。后者将虚拟机加载到其内存中并启动程序。但虚拟机可以在没有任何客户端的情况下使用。它提供了一个特殊的接口,允许直接与ELENA代码进行通信。因此,它可以用于从外部应用程序执行ELENA代码。

让我们从一个简单的ELENA代码开始。我们将声明一个打印消息的函数。

    public printingOut(arg)
    {
       console.writeLine("Hello from " + arg);
    }

代码相当直接,参数与文字字符串结合并在控制台中打印。为简单起见,未提供参数类型。

因为我们的代码不会独立使用,所以我们必须创建一个库——embedded1.prj并进行编译。

    >elena-cli.exe embedded1.prj    

此编译结果是一个编译后的库文件——embedded1.nl。我们可以从另一种语言调用其代码。我们将使用C#控制台应用程序来完成。

为此,我们还需要一个辅助类。

    public class ELENAVMWrapper
    {
        [DllImport(@"elenavm60.dll")]
        public static extern int ExecuteVMLA(string target, string arg, IntPtr output, int maxLength);
    
        [DllImport(@"elenavm60.dll")]
        public static extern int PrepareVMLA(string configName, string ns, string path, string exceptionHandler);
    
        public const string CONFIG_NAME = "vm_client";
        public const string EXCEPTION_HANDLER = "system'core_routines'critical_exception_handler";
    
        public static void Prepare(string ns, string path)
        {
            if (PrepareVMLA(CONFIG_NAME, ns, path, EXCEPTION_HANDLER) == -1)
                throw new Exception("The operation is failed");
        }
    
        public static void Execute(string target, string arg)
        {
            if (ExecuteVMLA(target, arg, , 0) == -1)
                throw new Exception("The operation is failed");
        }
    
        public static string ExecuteAndReturn(string target, string arg, int maxLength = 1024)
        {
            var ptr = Marshal.AllocHGlobal(maxLength);
            try 
            {
                int copied = ExecuteVMLA(target, arg, ptr, maxLength);
    
                return Marshal.PtrToStringUTF8(ptr, copied);
            }
            finally
            {
                Marshal.FreeHGlobal(ptr);
            }
        }
    }

在主程序中,我们编写一个简单的代码。

try
{
    ELENAVMWrapper.Prepare("embedded1", ".");
    ELENAVMWrapper.Execute("embedded1'printingOut", "C#");
}
catch (Exception e)
{
    Console.WriteLine(e);
}

让我们也编译并运行它。

    ELENA VM 6.0.36 (C)2022-2024 by Aleksey Rakov
    Initializing...
    Hello from C#

在下一个示例中,我们将对从ELENA程序外部传递的数组进行排序并将其返回。

我们的ELENA代码将再次包含一个带单个参数的函数。但这次我们将它从Json反序列化,对数组进行排序,然后再次以Json的形式返回。

    import extensions'dynamic;
    import algorithms;
    
    public string sorting(string json)
    {
       var array := json.fromJson();
    
       var sortedArr := array.quickSort();
    
       ^ sortedArr.toJson();
    } 

现在让我们编写C#代码。为了将数据解析为Json,我们将使用System.Text.Json。

    using System;
    using System.Text.Json;
    
    Console.WriteLine("Calling ELENA library from C#, Sample 3");
    
    try
    {
        ELENAVMWrapper.Prepare("embedded3", ".");
    
        var array = new int[] { 1, 5, -2, 56, -23 };
    
        var json = JsonSerializer.Serialize(array);
    
        string result = ELENAVMWrapper.ExecuteAndReturn("embedded3'sorting", json);
    
        Console.WriteLine($"{json} >> {result}");
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }

输出将类似:

    Calling ELENA library from C#, Sample 3
    ELENA VM 6.0.36 (C)2022-2024 by Aleksey Rakov
    Initializing...
    [1,5,-2,56,-23] >> [-23,-2,1,5,56]

语法

ELENA使用LL(1)语法。它是一种分析语法,意味着终结符*令牌*的角色由其在语句中的位置定义。因此,语法缺少关键字(而是使用用户定义的*属性*)。例如,可以完全在没有属性的情况下编写代码。

class
{
   field;

   method(param)
   {
   }
}

其中声明了一个名为class的类。它有一个名为field的字段和一个方法——method[1]

但在大多数情况下,需要提供额外信息。因此,令牌的角色由其位置决定。

class class;

singleton Tester
{
    do(var class class)
    {       
    }
}

public program()
{
    var class class := new class();

    Tester.do(class);
}

其中class用作声明新class的属性,用作标识符类型,也用作标识符。

更多示例

您可以在Rosseta Code中获取更多示例并了解更多关于ELENA语言的信息。

有任何问题吗?

请随时提问!

谁使用ELENA语言以及用于什么?

这是当人们看到关于新语言的任何内容时,一直萦绕在每个人心中的问题。

嗯,我使用该语言,并用ELENA语言开发业务程序。

我在ELENA中开发的程序控制着像福特、通用、(梅赛德斯)Fundo Estrela、(雪铁龙和标致)PSA Bank、(菲亚特、克莱斯勒和菲亚特二手车)FIDIS Bank这样的汽车金融银行收到的所有付款,这意味着如果您住在巴西并在这些制造商之一购买了汽车,那么他们的付款就是由ELENA编写的程序处理的。

我还为Bradesco银行和汇丰银行编写了一些程序来处理和生成付款数据。

ELENA中的开源项目

历史

  • 2024年7月10日
    • 更新以兼容ELENA 6.2
    • 添加了一个新部分,用于从外部代码调用代码。
    • 新的模板语句:if::is & foreach::in
  • 2024年2月11日
    • 更新以兼容ELENA 6.0
  • 2022年9月25日
    • 使用当前ELENA状态更新文章。
  • 2020年3月28日
    • 更新以兼容ELENA 5.0
  • 2019年12月27日
    • 文章更新。
  • 2019年6月24日
    • 新版本 - 4.1.0
    • 一些文章编辑。
  • 2019年4月4日
    • 添加新主题:文件操作、转换、.? 和 .\ 运算符。
  • 2019年1月9日
    • 更新以兼容ELENA 4.0
  • 2018年8月6日
    • 添加新主题:可枚举对象操作、混入。
  • 2018年6月13日
    • 更新以兼容ELENA 3.4
    • 修复拼写错误。
  • 2017年6月26日
    • 添加社区群组。
  • 2017年6月19日
    • 添加新版本编译器供下载。
  • 2017年6月9日
    • 将所有文章更新为新版本Elena语言。
    • 添加Elena语言的新版本供下载。
    • 添加“if-else模板”部分。
    • 添加“Elena中的开源项目”部分。
  • 2017年1月17日
    • 添加Code Project上新Elena文章的链接。
  • 2016年12月12日
    • 将Elena编译器更新到最新版本。
  • 2016年9月22日
    • 添加Rosetta Code链接。
  • 2016年9月12日
    • 文章创建。
© . All rights reserved.