不同的逻辑表达方式 #3
hmrg-grmh
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
不同的逻辑表达方式
在这里,所谓
fp
并非严格的函数式,而是尽可能地用符合逻辑本身结构的形式来表达逻辑。个人看法:命令式好在写时直观,但可能不便于阅读,其重要因素就是,它太容易破坏逻辑本身本来的结构,用户不得不结合上下文反复跳转(跳转后发现还要跳转)才能明白要做什么。
——也就是说,命令式如果可以做到不破坏这个结构,以不破坏这个结构的形式表达,那么便不会有不便阅读的坏处。在这篇文章
https://adventures.michaelfbryan.com/posts/rust-best-practices/bad-habits/
里也有一个命令式相对而言可以更好表达事情本来的逻辑结构的例子。SHell 示例(使用
kk
的下载脚本)之所以用下载脚本示例,是因为像这样的下载或构建脚本,一般是面向于并不了解 SHell 本身机制的用户编写的。如果引入子进程、 SHell 上的函数的特性等概念,就会破坏这个原则。但如果不引入这些,使用 SHell 编写的代码就难以做到不破坏事物本身的逻辑。如果你不明白 SHell 本身的一些特性,或许原脚本更适合你;如果你明白(子进程、返回码,等),那么或许第二个版本示例是更具备可读性的。(当然了我也并不能保证它足够好——在此我只是尽可能保护原本逻辑的结构。😋)
原脚本内容
可用
curl -sfL https://get-kk.kubesphere.io
取得:其实,下面的代码,乃至这整篇文章,都是因为,我为了确保自己读懂了上面的代码到底在做什么,而制作的——我为了确保这件事,所以就打算「用自己的风格实现以下然后使用自己写出来的代码」这样子了。。。。
风格更改
在解析 api 时使用了 Python 。版本
2
3
应该都可以用。(所以它不一定具备实用性。不过这里并不是要争取实用性(当然也不是说实用性必然要破坏逻辑结构))
其实 SHell 的许多设计也是很不错的。比如:
&&
和||
结尾符。 SHell 没有if
也就是分支逻辑,if
关键字本身也依然是根据条件部分的命令的退出码来作不同触发的,使用这两个结尾符就是其最朴素的写法。当子进程被设置为错误退出,比如(exit 233)
,这时候想让当前进程也以同样退出码退出(但只在错误退出时),就只需要紧接着在后面补充|| { exit $? ; }
(大括号与分号可一起省略),最终就是这样:(exit 233) || { exit $? ; }
,就能指定当前进程因为这个子进程的错误退出也错误退出了。这其实挺像Java
的异常抛出,不过显然是更简洁的了(几乎就是只是描述了逻辑本身)。但也有非常糟糕的地方,比如:
`# xxx`
一定程度上可以有行内注释的效果)上述代码可以放在函数定义里或者直接在你的 SHell 界面上粘贴使用(需要在一对大括号之间(粘贴的部分和前后大括号之间还要各有至少一个空白符))。
代码压缩:应该也可以用这个逻辑简单地变成一行而不影响使用(那个
#! /bin/sh
除外):所有回车都替换成空格、连续多个空格替换成一个空格。多说几句
一句
有很多脚本喜欢把函数用于划分模块,并且不介意函数套函数(造成深层次的隐藏细节);本来看代码也不难明白的很简单的一部分逻辑,非要给个名字装进定义,结果回头却只会被使用一次……说实话,这真的很糟糕。我保证,我并不会这样做。我认为这是无意义的抽象。因为抽象是为了让代码易读,而不是难读的。不能为了抽象而抽象。
两句
我给了自己一些约束,比如一些命名。那个
vals__
的名称必须看起来足够特殊,因为它的行为就足够特殊(改变全局共享的变量)。这其实也是一个模块,但我要求自己绝不可以把它弄成隐藏细节的 HELL ——我以前做过这事儿,然后我就再也不想看那些代码,并根据需求重新实现了更简洁的代码——事情就是这么简单。。。三句
我不排斥命令式,这从上面的代码也能看出来。
while
的部分是做了个重试机制,而这里完全可以用尾递归来写(我知道怎么利用exec
来保证不爆栈);而且我甚至都没有对所有只会用一次的变量使用readonly
——首先是因为这会破坏代码简洁(而足够简洁时为了配合机器引入多此一举的表示就不利于在字符界面阅读代码了),再就是因为不是所有 SHell 都支持这个(会破坏兼容性)。还是那句话,形式(代码)要合乎本质(逻辑),这才是代码可读性的标准——我是说,在满足各项要求的前提下,尽可能地靠近这一点。(人类不是机器所以没有理由放弃指望人的自觉,无非就是需要为此建立很多条件(这也需要破坏一些条件)而已。)四句
我很喜欢管道,它在一些时候不太好(因为会多启一个进程而不太利好变量向外传递),但大部分时候它很有用:管道对于 UnixSHell 就如同 Monad 对于 Haskell 。因为它不会隐藏,每一步我都可以检查,我可以把每一步的副作用完全限制在
stdin
stdout
stderr
的范围内(从而确保它们的可控与临时性),然后呢,我只需要这样的思路就好:我要用几个关键信息生成一系列命令,然后去触发它们的执行。这已经可以解决大部分问题了,而且通过管道使用xargs
来并行(并行进程)做事是很方便的,它比for
/while
要方便而且安全,特别是直接指定一个数字就能控制并发度这一点。五句
一如上文,我讨厌的做法就是,让形式不合乎于事实的逻辑的方案。
if command ; then
还有for x in $(seq 666);
都是如此:if if cmd1 ; then cmd2; cmd3; else cmd4; cmd5; fi
和cmd1 && { cmd2; cmd3; } || { cmd4; cmd5; }
是一样的,然而前者伪装出了一个 条件表达式 (并诱惑新手误以为 SHell 上有 Bool 类型),隐藏了那部分其实就是个命令的真相,同时啰嗦了代码(而且if-fi
显然不如{}
看着方便)。多分支的情况尽可能使用case
,这会极大地好过使用if-fi
。for x in $(seq 4) ;
其实就是for x in 1 2 3 4 ;
,但很多人对待新手的疑惑时只会有「这是固定写法你甭关心」这样的回答。难道说死记硬背比举一反三能够具有更好的编程能力?我不晓得。其实for
本身没什么问题,无非不用它就要用seq 4 | while read x ;
(或者awk
)来达到一样的效果,后者启动的进程树也并不完全和前者一致。在类似这样的时候,for
会很方便:for x in "$@" ;
——就是说,在in
后面的内容需要是一个(可能意味着多个值的)变量,或者多个写好的值,这样的时候。另外for
还有一种类似于 C 语言风格的写法,这里也要清楚,双括号其实是let
命令的变形。这个问题不大,只是双括号内部的语法和外部不太一样,有时候可能用while
更能保护可读性。一行命令
下面的示例只有要做的工作和上面一样,但软件的功能细节已经完全不一样了。我会删掉很多提示性的功能(或者只是摧毁它原本的时机方面的设计),从而把一个脚本变成 一行命令 。
概念辨析: 一行命令
e.g.
这是 一行命令 :
echo a
这也是 一行命令 :
这是 三行命令 :
这却是 一行命令 :
当然这也是 一行命令 :
这却是 一行命令 :
这整个也是 一行命令 :
这当然也是 一行命令 :
另外这也是 一行命令 :
即便增加一些换行符和空格来格式化一下也仍然是 一行命令 :
what ?!
其实这个叫法我只在我的代码里这样用。它或许足够严谨(不严谨的只会是我目前的描述),但不一定是 Bash 的正规的规定。
其实若从前面的示例就能明白我想表达啥的话,这个部分不看也无妨……🦥
我做了这样的一些规定:
Enter
提交进程(及其子进程)。Enter
不会提交进程。stdin
、stdout
、stderr
, 分别叫作 标准输入 、 标准输出 、 标准错误 。(这个其实不属于我的规定。。但下面会用到)行结尾符:一行名令结束的地方可以(或是视情况必须)有的符号:
;
:用来结束一行命令。如果Enter
被输入时之前的已经算是 一行命令 则会自动补充。这个输入会提交命令执行,同时阻塞当前进程,直到执行完毕,当前进程才可以继续。(如果当前进程是个 SHell 则阻塞消失的标志就是命令提示符被打印以提示用户可以继续输入命令以使用当前 SHell 进程。)
&
:和前者一样,只是会把这一行命令所启动的进程(及它的子进程)变为 当前进程 的后台进程(但该进程仍然是当前进程的子进程)。可以用jobs
命令列出。( 后台进程 不会阻塞当前进程,但如果当前进程是 SHell ,后台进程默认仍然与当前 SHell 进程共用同样的标准输出和标准错误的终端,这会造成名为 命令行撕裂 的现象。)
条结尾符:一条命令结束的地方可以有的符号;若不打算作为行结尾则是必须要有的符号:
|
:管道,让前面那条命令会启动的进程的标准输出的内容,就成为其后那条的标准输入。管道会让被链接的各条命令所启动的进程,进入一个统一的进程下并作为其子进程。管道链优先级高于后两者。&&
:让前面那条在正确退出(退出码为0
)则后面那条才执行,否则后面那条不执行并视为以同样退出码退出。||
:让前面那条在错误退出(退出码为1-255
)则后面那条才执行,否则后面那条不执行并视为以同样退出码(这里就只可能是0
了)退出。一行代码
上面的命令真的只是一行!不信可以试试,你只有到最后分号那行敲回车的时候解释器才会理你!!🐊
其实前面搞一大堆就是为了得到一个地址然后能够执行这个:
curl -fL -- https://ghproxy.com/https://github.com/kubesphere/kubekey/releases/download/v1.2.1/kubekey-v1.2.1-linux-amd64.tar.gz | tar -xzf-
得到链接的阶段比较有技术含量的就是获取版本了:
这样就能标准输出得到一个当前最新稳定版版本号了。这里没有搞重试。之所以没有用赋值变量展开来达到分支效果是因为那种写法不会节省时间。
其次或许就是那个
KKZONE
了,不过这里我只是用了case
制作分支,以及用了不一样的魔法而已。——当然了,我只是说这个下载脚本里的KKZONE
选择区域功能的实现。kk
这个软件内部也被这个变量统一影响的设计则是非常天才的。Beta Was this translation helpful? Give feedback.
All reactions