的J - 181 190 170炭
这是一场噩梦。我从头开始重写了两次,因为它一直困扰着我。该函数采用单个字符串参数,输出到STDOUT。
(0&$`((2{.{:@>&.>)((j{.]),-i@=`p@.~:~/@[,]}.~#@p+j=.0{p I.@E.])i 5;@}.&,'/';"0;&.>)@.(2<#)@}.[4:1!:2~{:@>@p=.>@{.@[)@((0;(0,:~1 0,.2);'\';&<1 0)<;._1@;:'/'&,)i=. ::](^:_)
为了说明,我将其分解为子表达式。
i =. ::](^:_))
parse =: ((0;(0,:~1 0,.2);'\';&<1 0)<;._1@;:'/'&,)
print =: 4:1!:2~{:@>@p=.>@{.@[
eval =: 0&$`((2{.{:@>&.>)sub 5;@}.&,'/';"0;&.>)@.(2<#)@}.
sub =: ((j{.]),-i@=`p@.~:~/@[,]}.~#@p+j=.0{p I.@E.])i
interp =: (eval [ print) @ parse i
i
(iterate的缩写)是一个副词。它在左侧使用一个动词自变量,并返回一个动词(f)i
,将其应用于自变量后会f
重复应用于该自变量,直到发生以下两种情况之一:找到一个固定点(y = f y
),或者抛出一个错误。定点行为是固有的^:_
,并::]
进行错误处理。
parse
将输入标记化为我所说的 半解析形式,然后将其未转义的'/'。它将转义的反斜杠绑定到其字符,但不会消除反斜杠,因此我们可以根据需要将其还原或完成。
有趣的工作大部分发生在;:
。这是一个顺序机器解释器原语,(0;(0,:~1 0,.2);'\';&<1 0)
左边是对机器()的描述,右边是要解析的内容。这会进行标记化。我会注意到,这台特定的机器实际上将第一个字符视为非特殊字符,即使它是\
并且应该绑定。我这样做的原因有几个:(1)状态表更简单,因此可以进一步打高尔夫球;(2)我们可以轻松地在前面添加一个虚拟字符来规避问题;(3)虚拟字符无需额外费用就可以进行一半解析,因此接下来我可以用它来设置剪切阶段。
我们还可以<;._1
在未转义/
的字符上剪切标记化的结果(这是我选择作为第一个字符)。这很方便out/patt/repl/rest
一步一步从所有输出,模式和替换中进行输出,但是不幸的是,这也削减了程序的其余部分,而我们需要这些/
程序保持不变。我在期间将它们拼接起来eval
,因为不理<;._1
会它们最终会花费更多。
叉子会(eval [ print)
根据的副作用执行print
结果parse
,然后运行eval
。print
是一个简单的动词,它打开第一个框(我们确定可以输出的框),完成对它的解析,然后将其发送到STDOUT。但是,我们也借此机会定义了一个实用动词p
。
p
被定义为>@{.@[
,因此它采用其左arg(如果仅给出一个arg,其作用类似于标识),采用其第一项(给出标量时的标识),然后将其取消装箱(如果已取消装箱,则标识)。这将非常方便sub
。
eval
评估已处理程序的其余部分。如果我们没有完整的模式或完整的替换,eval
请将其扔掉并仅返回一个空列表,该列表将通过在下一次迭代中产生;:
(from parse
)错误来终止评估。否则,eval
完全解析模式并进行替换,纠正源的其余部分,然后将两者都传递给sub
。爆炸:
@}. NB. throw out printed part
@.(2<#) NB. if we have a pattern and repl:
2{. NB. take the first two cuts:
&.> NB. in each cut:
{:@> NB. drop escaping \ from chars
( ) NB. (these are pattern and repl)
&.> NB. in each cut:
; NB. revert to source form
'/';"0 NB. attach a / to each cut
&, NB. linearize (/ before each cut)
5 }. NB. drop '/pattern/repl/'
;@ NB. splice together
( sub ) NB. feed these into sub
` NB. else:
0&$ NB. truncate to an empty list
sub
是发生一轮(可能是无限次)替换的地方。由于我们的设置方式eval
,来源是右侧参数,而模式和替换在左侧捆绑在一起。由于参数是这样排序的,并且我们知道模式和替换在轮次替换中不会改变,因此我们可以使用的另一个功能(i
事实上,它仅修改正确的参数,并始终向左传递)来委托J需要担心跟踪状态。
但是,有两个麻烦点。首先是J动词最多可以有两个参数,因此我们没有一种简单的方法来访问捆绑在一起的任何东西,例如模式和替换。通过巧妙地使用p
我们定义的实用程序,这并不是什么大问题。实际上,p
由于其>@{.@[
定义,我们只需使用即可访问一个字符中的模式:Left arg的First项的Unbox。获取替换项比较麻烦,但是最短的方法是p&|.
,比手动取出要短2个字符。
第二个问题是i
在固定点上退出而不是永远循环,如果模式和替换相等,并且您进行了替换,则看起来像是J的固定点。如果我们检测到它们相等,那就是这-i@=`p@.~:~/
部分,替换p&|.
。
p E.] NB. string search, patt in src
I.@ NB. indices of matches
0{ NB. take the first (error if none)
j=. NB. assign to j for later use
#@p+ NB. add length of pattern
]}.~ NB. drop that many chars from src
/@[ NB. between patt and repl:
~ NB. patt as right arg, repl as left
@.~: NB. if equal:
-i@= NB. loop forever
`p NB. else: return repl
(j{.]) NB. first j chars of src
, , NB. append all together
( )i NB. iterate
由于使用,该循环重复进行i
,直到超出sub
出现错误。据我所知,这只能在我们字符不足,抛出不完整的图案和替换集时发生。
关于此高尔夫的趣闻:
- 一次,使用
;:
比手动遍历字符串短。
0{
应该有机会出错之前 sub
进入无限循环,,因此,如果模式与替换匹配,但永远不会出现在源的其余部分中,则应该可以正常工作。但是,这可能是也可能不是未指定的行为,因为我在文档中都找不到引文。哎呀
- 键盘中断在运行功能中被视为自发错误。但是,由于
i
,这些错误也会被捕获。根据您按下Ctrl + C的时间,您可能会:
- 退出永久否定循环,退出错误
sub
循环,尝试将数字连接到字符串,然后然后继续////解释错误,就好像您用无限次的自身替换了字符串一样。
- 离开
sub
一半,继续解释半字幕///表达式。
- 脱离解释器,然后将未评估的程序//返回给REPL(尽管不是STDOUT)。
用法示例:
f=:(0&$`((2{.{:@>&.>)((j{.]),-i@=`p@.~:~/@[,]}.~#@p+j=.0{p I.@E.])i 5;@}.&,'/';"0;&.>)@.(2<#)@}.[4:1!:2~{:@>@p=.>@{.@[)@((0;(0,:~1 0,.2);'\';&<1 0)<;._1@;:'/'&,)i=. ::](^:_)
f 'no'
no
f '/ world! world!/Hello,/ world! world! world!'
Hello, world!
f '/foo/Hello, world!//B\/\\R/foo/B/\R'
Hello, world!
f '//' NB. empty string
f '/\\/good/\/'
good
/-/World//--/Hello//--W/--, w/---!
不去爱的种种?(尝试从末尾删除破折号)