北大侠客行MUD论坛

 找回密码
 注册
搜索
热搜: 新手 wiki 升级
查看: 126|回复: 10

杰哥乱弹琴之细说函数

[复制链接]
发表于 3 天前 | 显示全部楼层 |阅读模式
函数和变量是在写机器人时最重要的工具。

普通人学写机器人大多是从函数和变量开始的。

如果要想能在写机器人时有质变,有顿悟,函数是最重要的一个门槛。

所以,我在这里也细细的扯一下函数。

首先,让我们问自己几个问题

什么是函数?

我们为什么要用函数?

我们可以怎么用函数?

使用函数时是否有什么技巧?

北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 3 天前 | 显示全部楼层
什么是函数?

函数最初是数学上的概念,描述的是数与数,集合与集合直接的关系。

“函数最初是一个变化的量如何依赖另一个量的理想化。例如,特定时间行星的位置可以视为是行星的位置对时间的函数。“函数”一词作为数学概念是由莱布尼茨首先引入的。从历史上看,这个概念是在17世纪末用无穷微积分来阐述的,直到19世纪,所考虑的函数都是可微的。函数的概念于19世纪末在集合论中被形式化,这大大扩展了这个概念的应用领域。”

引自wiki

在计算机领域的话,函数的功能和概念是随着语言和发展阶段有不同的概念的。

我本人除了当年在学习机上玩票玩的小海龟/basic语言,正儿巴经的写代码应该是97年有了自己的电脑之后自学的VB语言。在VB语言里,代码分为 过程 和 函数。过程是没有返回值的,函数是有返回值的。这已经比当年basic goto 行号高科技无数倍了。

而后的主流语言里,基本不太区分过程和函数,过程就是返回值为空的函数。

在这个阶段,主流是 函数是可以有返回值的,便于复用的代码段。

下一个阶段,大体就是JAVA和C#为主导的面向对象(OOP)潮流了。很多脚本语言,比如lua,python其实也在这个阶段加入了OOP大军。在面向对象语言里,函数并不受重视,类才是一等公民。类代表了一系列的固定格式数据(实例),以及用于处理数据的函数(方法),函数大体依附于类而存在。

当然,现在一种很主流的看法,面向对象在一些领域很银弹(人月神话的说法),但在更多的领域未必很适用。所以面向过程的编程范式还在很多领域,特别是脚本语言中蓬勃发展。逐渐引入了匿名函数,Lamda表达式等重要工具,甚至开始流行函数式的开发范式。在这个过程中,有一个重要的标志,就是函数是一等公民,也就是函数和字符串,数字等类型一样是主要的基础数据,可以存在各种变量里,可以存放在各种数据结构里。

大概是

var funca=function(a){
    return a+1
}

var funclist=[function(a){
    return a+1
},function(a){
    return a*1
}]

这种形式。

特别重要的是,函数因为是一等公民,可以作为函数本身的参数和返回值

比如

var funccheck=function(condition){
    if (condtion()){
        return function(){echo("ok")}
    }
    return null
}

这种形式

这种面向过程的发展中,函数大概是这么个东西

函数是一种特殊的数据(内存里0和1),程序能运行这种格式的数据,这种数据也可以作为参数和返回结果。

我的理解,对于写机器人来说

面向对象的范式过于强调抽象和复用
函数式变成过于强调数据

只有面向过程,函数第一成员的范式,更适合机器人这种 触发/处理的场景

北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 3 天前 | 显示全部楼层
我们为什么要使用函数?

函数是写机器人的重要工具,那么,函数是必须的吗?

很遗憾,不是。

最原始的客户端中,比如低版本的ZMUD,本身未必有成熟的函数的概念。

函数是因为太适合写机器人,太好用,被我门大量引入写机器人。

函数的优点包括

1.便于复用。比如很多地方需要将中文转数字,你不用函数一次次复制粘贴怕不要类死
2.便于封装组织。几百行的代码可能那一维护,但如果把代码封装到起了合适函数名的函数里,然后之维护几十个函数,难度就降低了很多。拆封为函数也便于分段对代码进行测试
3.变为控制复杂度。我一直强调,写机器人是一个工程学的问题。工程学主要的问题,不是算法的难度和性能的高低,而是如何降低代码的复杂度和维护的难度,降低出BUG的概率,尽可能延长项目的寿命(维护成本超过维护项目的收益,这个项目就死了)

北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 3 天前 | 显示全部楼层
我们怎么用好函数?

既然函数这么重要,我们自然要用好函数

在写机器人中,函数的作用主要体现在

1.清晰化结构,增加可维护性
2.建立异步概念,从被动型机器人转向主动型机器人
3.利用函数,实现组合,避免继承
4.利用函数数组,实现优先级任务


北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 3 天前 | 显示全部楼层
清晰化结构,增加可维护性

在开始深入之前,我们简单的了解下WEB,也就是互联网开发。

所谓的WEB开发,本质就是一个面对Request/Response进行开发。

浏览器想服务器发送一个请求,请求的内容是一段文本和相应的会话信息(上下文),服务器接收到请求后,对浏览器作出一定的反馈(文本和上下文信息)

很明显,这个很接近于MUD机器的状态,不过MUD服务器是浏览器,MUD客户端是WEB服务器,进行一个个触发/回馈的流程。

在这个基础上,我们能参考很多WEB开发里线程的经验。

比如WEB开发有一个很经典(老旧)的架构,叫MVC

Model(模型)-View(视图)-Controller(控制器)

其中

Model是业务逻辑,一般是最脏最复杂的部分,比如各种寻找,战斗,处理,都可以认为是Model

Controller是控制器,代码很轻,负责接受请求,根据简单判断,调用和是的模型和必要辅助函数,再通过View输出内容给Mud服务器

View 是渲染视图,将业务数据转化为浏览器可以理解的形式,对于Mud机器人来说,就是一个抽象出俩的发送函数,能够将指令以合适的格式频率,进行适当转义发送给服务器。

控制器要尽可能的轻,能一眼看出逻辑,避免逻辑bug发生,就是一个简单的函数

Model一定很重,所以要整理出和是的入口,必要时可以写一定的测试代码进行测试,同时注重可复用性,一般整理为多个函数,为了测试方便,可以将整个系统环境作为参数传入,这样方便测试的时候传测试数据

View尽量作为独立通用模块,一个标准的输出函数,不光在机器人中可以使用,在客户端的别名/触发器/甚至输入框都能方便的进行程序化的输出。

北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 3 天前 | 显示全部楼层
什么是异步

异步是一个现代编程中很常见的概念,相对于同步而言。

同步基本可以认为是串行,我们一般写代码都是同步的,一行一行执行,直到最后输出结果或者中间Return

异步则不同,异步可以认为是代码执行完成返回了,但工作还没有完成,通过各种机制等工作完成再执行其他操作

一般计算机开发中,由于设计读取文件/网络等长时间操作,引入的异步的概念来充分利用资源,避免计算性能被IO操作卡死。

所以,从正统概念来说,写机器人没有啥异步的概念。

之所以会在写机器人中引入异步的概念,是为了能够将本来并不同步的代码,能够以接近同步的方式来写,降低工程复杂度,降低维护成本。

举个例子,去银行取100G然后去铁匠铺修武器,完全可以不用引入异步的概念,用一对触发作这个流程。

也可以写成这样的伪异步代码
function repqir(){
moveto("bank")
qu("100 gold")
moveto("tiejiangpu")
repair("mysword")
}
可维护性有肉眼可见的数量级的差距

机器人异步化的代码就是把将来计划的事情,建立一个任务队列,依次执行。

当然,异步化不是简单的换个代码的写法就行。最基本的,由于异步return不代表工作完成,还需要有个显式的控制权转移的过程。就是明确,我这里的事情完成了,该交给别人来处理了。

一般,这种时候,我们会自然的引入回调的概念

“回调函数或简称回调(callback),是计算机编程中对某一段可执行代码的引用,它被作为参数传递给另一段代码;预期这段代码将回调(执行)这个回调函数作为自己工作的一部分。 这种执行可以是即时的,如在同步回调之中;也可以在后来的时间点上发生,如在异步回调之中。”

很好,很快我们就会见识到Nodejs初期的问题了,回调地狱

回调太多太乱太杂,无法管理。

不过好在,既然我们知道nodejs遇到过这个问题,我们就能知道怎么去处理了。

NodeJS初期有个很有名的包叫做Async.js

就是把一些列的回调,放在一个数组内,然后以这样的形式依次调用


async.filter(['file1','file2','file3'], function(filePath, callback) {
  fs.access(filePath, function(err) {
    callback(null, !err)
  });
}, function(err, results) {
    // results now equals an array of the existing files
});

作为第一个成功的把异步代码同步化的库,async.js可以说功德无量。

async.js的概念就是把控制函数传给回调列表,依次调用,回调中主动释放控制权,直到调用结束。

当然,既然说是初期,那后期的nodejs肯定有更成熟好用的方案了。那就是Promise

参考mdn,大概是

一个 Promise 是一个代理,它代表一个在创建 promise 时不一定已知的值。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 promise,以便在将来的某个时间点提供该值。

一个 Promise 必然处于以下几种状态之一:

待定(pending):初始状态,既没有被兑现,也没有被拒绝。
已兑现(fulfilled):意味着操作成功完成。
已拒绝(rejected):意味着操作失败。

就是可以将一系列代码串成一个Promise,调用后依次执行,直到成功或者失败

大概是

myPromise
  .then((value) => `${value} and bar`)
  .then((value) => `${value} and bar again`)
  .then((value) => `${value} and again`)
  .then((value) => `${value} and again`)
  .then((value) => {
    console.log(value);
  })
  .catch((err) => {
    console.error(err);
  });

当然,学习nodejs,是为了他山之石,不是为了照抄。

nodejs和mud机器人的场景并不完全一致

对于我而言,显示建立了一个自动(多层回调队列)机制,然后绑定再全局变量上。

这样可以通过

App.PushCommand(command1,command2,command3)

的形式规划指令,然后通过App.Next,App.Finish,App.Fail三个指令来进行流程控制。

以我新机器的代码威力

大概是

    //前往小二处
    Beiqi.Go = function () {
        let task = Beiqi.Data.Current
        $.PushCommands(
            $.Prepare("", preparedata),
            $.To(task.InfoLoc),
            $.Nobusy(),
            $.Ask(task.InfoID, task.Name + "的事", 1),
            $.Function(Beiqi.Check),
        ).
            WithFailCommand(nextCommand).
            WithFinishCommand(nextCommand)
        $.Next()
    }
    Beiqi.Next = function () {
        if (Beiqi.Data.Current) {
            Beiqi.Data.Current.Last = $.Now()
        }
        $.Next()
    }
    let nextCommand = $.Function(Beiqi.Next)

的形式,很接近于写同步代码了,只要将预设的指令(本质是一个个特殊的函数),外加参数,组成数组,就能写各种复杂机器人了,难度复杂度和耗时直接降低几个数量级


北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 3 天前 | 显示全部楼层
利用函数,实现组合,避免继承

组合优于继承,是一件很常见的话了。

归根到底,这是对面向对象的一种反思。

继承是一种基于纯逻辑推定的关系,目的是为了理顺逻辑概念里的派生关系,所以在GUI这种纯逻辑问题里很好用,一但设计到了实际业务问题,因为业务问题的多变不固定和复杂,使得没法很好的抽象,抽象出的关系也经常被变更。

而组合就是把一系列对象打包,用这些对象的对应部分组合成一个新的时和我们需要的东西。

而在函数第一性的语言里,很明显最好的组合对象就是函数。

这点我最喜欢Go语言的interface,只要你有指定格式的几个函数,你就能当同一样东西来用,也就是传说中的鸭子对象。

言归正传。

在机器里,最典型的组合,就是复杂制定,就是移动 和战斗了

对于移动,就是一个大字典(table/dict/table)

比如我的移动对象有OnRoom,OnNext,OnRetry,OnArrive,大概是

    class Move {
        StartCommand = ""
        Data = {}
        Retry = DefaultMoveRetry
        Next = DefaultMoveNext
        OnRoom = DefaultMoveOnRoom
        OnArrive = DefaultMoveOnArrive
        Vehicle = DefaultVehicle
        OnFinish = module.DefaultOnFinish
        OnCancel = module.DefaultOnCancel
        OnInitTags = DefaultOnInitTags
        OnStepTimeout = DefaultOnStepTimeout
        OnStepFinsih = DefaultMoveOnStepFinish
        MapperOptionCreator = DefaultMapperOptionCreator
    }

这些都是函数,很明显,我只要替换调不同的函数,就能实现不同的移动

从遍历,移动,探索,深度遍历,都可以用同一套逻辑的实现。

具体使用时特化成不同的同格式函数并替换就行

当然,为了实现异步和功能,很多的函数返回值也是函数

我的战斗对象也一样

    class Combat {
        constructor(position, plan) {
            this.Position = position
            this.Plan = plan
        }
        Data = null
        Interval = module.DefaultInterval
        Position = null
        Combating = false
        Target = ""
        StartAt = 0
        Plan = null
        Ticker = module.DefaultTicker
        OnStop = module.DefaultOnStop
    }

很明显,有Ticker(周期判断)OnStop(结束回调)

在实际使用替换掉函数就行。

在写机器人时,通过一个含有指定格式函数的表,就可以写出搞通用型/复用性/后向兼容性 的代码模块了


北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 3 天前 | 显示全部楼层
利用函数数组,实现优先级任务

最后来一个比较实务的模块。

每个机器的准备工作都很重要。

不同类型的任务(练功/赚钱/打架)的任务都会有所不同。

所以最好的方式自然是做好预设的异步回调模块,然后创建不同的数组,比如:

    //注册通用的准备组
    let common = App.Proposals.NewProposalGroup(
        "eatyao",
        "cash",
        "gold",
        "qu",
        "silver",
        "coin",
        "jinchuanyao",
        "yangjingdan",
        "food",
        "drink",
        "inspire",
        "dazuo",
        "heal",
        "tuna",
        "dispel",
        "assets",
        "refund",
        "bond",
        "item",
        "repair",
        "dropwp",
        "summon",
    )
    App.Proposals.Register("", common)
    App.Proposals.Register("common", common)
    //注册通用带学习的准备组
    App.Proposals.Register("commonWithStudy", App.Proposals.NewProposalGroup("common", "study", "jiqu"))
    //注册通用带学习和放弃经验的准备组
    App.Proposals.Register("commonWithExp", App.Proposals.NewProposalGroup("common", "exp", "study", "jiqu"))
    //注册准备指令

而如果每次检查都执行所有的代码明显不合适,所以必然要利用函数的返回值

最简单的,就是每个函数返回true/false,返回false继续,返回 true继续,这就是个典型的责任链模式了。

但返回true和false,代码还需要写很多处理代码,特别是一旦没写好很容易造成需要改结构和所有相关代码的地方

所以我们不如改返回值

如果需要检查不需要处理,就返回空 (null)

如果需要中断,拿就返回一个函数(异步回调)

整体的准备代码就是

循环一下传入的准备数组。

每次执行一个准备函数。

返回空,就检查下一个。

返回函数,就执行函数,指向完了再重头循环。

如果全部检查完了,就开始任务主体工作。


北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 3 天前 | 显示全部楼层
打完收工,接娃下课
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
发表于 3 天前 | 显示全部楼层
mobai
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|北大侠客行MUD ( 京ICP备16065414号-1 )

GMT+8, 2025-3-26 09:55 PM , Processed in 0.012181 second(s), 15 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表