跳至正文
LNN的博客!

“ruby,一款好用的 esolang”

$><<(""<<~-~-~-~-~-~-~-~-(-~-~-~-~-~([]<=>[])<<-~-~-~-~([]<=>[]))<<-~-~-~-~-~(-~
-~-~([]<=>[])<<-~-~-~-~-~([]<=>[]))<<~-~-~-~-(-~-~-~-~-~-~-~([]<=>[])<<-~-~-~-~(
[]<=>[]))<<~-~-~-~-(-~-~-~-~-~-~-~([]<=>[])<<-~-~-~-~([]<=>[]))<<~-(-~-~-~-~-~-~
-~([]<=>[])<<-~-~-~-~([]<=>[]))<<~-~-~-~-(-~-~-~([]<=>[])<<-~-~-~-~([]<=>[]))<<(
([[]]<=>[])<<-~-~-~-~-~([]<=>[]))<<-~-~-~-~-~-~-~(-~-~-~-~-~([]<=>[])<<-~-~-~-~(
[]<=>[]))<<~-(-~-~-~-~-~-~-~([]<=>[])<<-~-~-~-~([]<=>[]))<<-~-~(-~-~-~-~-~-~-~([
]<=>[])<<-~-~-~-~([]<=>[]))<<~-~-~-~-(-~-~-~-~-~-~-~([]<=>[])<<-~-~-~-~([]<=>[])
)<<-~-~-~-~(-~-~-~([]<=>[])<<-~-~-~-~-~([]<=>[]))<<-~(([[]]<=>[])<<-~-~-~-~-~([
]<=>[]))<<-~-~-~-~-~-~-~-~-~-~([]<=>[]))
Hello, World!

Ruby 语言有很多有趣的特性:包含循环引用的数据之间可以比较是否相等,重载运算符就是定义以运算符为名称的方法,可以用 字符串 << 字符串或字符编码 向字符串追加内容,整数可以像比特组成的数组一样被索引和切片,“全局函数”实际上是 Kernel 模块的私有方法,Objectdisplay 方法等价于 print 函数……

Esoteric Ruby

前些天,热爱 Esolang 的群友“预防”尝试写出不包含字母的 Ruby 程序。首先他想到了 String#<<

("" << 72 << 101 << 108 << 108 << 111 << 44 << 32
    << 87 << 111 << 114 << 108 << 100 << 33 << 10).display

(如果启用了 frozen_string_literal,需将 "" 改为 +"":一元正号运算符可以复制冻结的字符串,得到一个未冻结的副本。)

如何不使用字母调用 Object#display 呢?试试 Symbol

"" << 100 << 105 << 115 << 112 << 108 << 97 << 121 # => "display"
:"#{"" << 100 << 105 << 115 << 112 << 108 << 97 << 121}" # => :display

然而,如何通过方法名的 Symbol 调用方法?正常情况下……

"Hello, World!\n".send(:display)
"Hello, World!\n".method(:display).call
"Hello, World!\n".method(:display).()
"Hello, World!\n".method(:display)[]
String.instance_method(:display).bind("Hello, World!\n").call
String.instance_method(:display).bind_call("Hello, World!\n")
lambda(&:display).call("Hello, World!\n")
lambda(&:display).("Hello, World!\n")
lambda(&:display)["Hello, World!\n"]

不管用怎样刁钻的方式调用方法,都会引入新的方法/函数调用。但后来我发现:

def greet(&proc)
  proc["Hello, World!\n"]
end
greet(&:display) # 输出 Hello, World!

通过定义一个接受块的函数,可以在不调用方法的情况下Symbol 转换成 Proclambda)。而这个 greet 函数可以写成箭头函数

greet = -> (&proc) { proc["Hello, World!\n"] }
greet.(&:display) # 输出 Hello, World!

把箭头函数内联,改一下变量名,就得到:

(-> (&_) { _["Hello, World!\n"] }).(&:display) # 输出 Hello, World!

再把字符串和 Symbol 替换掉:

(->(&_){_[""<<72<<101<<108<<108<<111<<44<<32<<87<<111<<114<<108<<100<<33<<10]})
  .(&:"#{""<<100<<105<<115<<112<<108<<97<<121}")

我们就得到了没有字母的 Hello World 程序

然而我们随即发现全局变量 $> 相当于常量 STDOUT

STDOUT << "Hello, World!\n"
$> << "Hello, World!\n"

于是……

$> << (""<<72<<101<<108<<108<<111<<44<<32<<87<<111<<114<<108<<100<<33<<10)

根本不需要什么箭头函数。

RBFxck

于是我们打算提高难度:不能用数字。

如何用符号获得整数?可以用比较运算符 <=>。数组 [] 等于 [][[]] 大于 []。于是:

[] <=> [] # => 0
[[]] <=> [] # => 1
[] <=> [[]] # => -1

用左移操作可以获得更多整数:

([[]] <=> []) << ([[]] <=> [])                    # 1 << 1      => 2
([[]] <=> []) << ([[]] <=> []) | ([[]] <=> [])    # 1 << 1 | 1  => 3
([[]] <=> []) << (([[]] <=> []) << ([[]] <=> [])) # 1 << (1<<1) => 4

不过后来我们发现利用一元负号按位取反运算更方便:

~1    # => -2
-~1   # => 2
~-~1  # => -3
-~-~1 # => 3

-~([[]] <=> []) # => 2
-~-~([[]] <=> []) # => 3
-~-~-~([[]] <=> []) # => 4

有了上次的经验,经过反复调整,我完成了这样一个整数/字符串编码程序:

def shortest(*a)
  a.min_by(&:length)
end

def rbfuck_int(n)
  return "([[]]<=>[])" if n == 1
  return "([]<=>[[]])" if n == -1
  return shortest("-#{rbfuck_int(-n)}".sub(/^--/, ""), "~#{rbfuck_int(~n)}".sub(/^~~/, "")) if n.negative?
  return "#{"-~" * n}([]<=>[])" if n <= 13

  threshold = 8
  bitl = threshold.bit_length
  if n[0, bitl] >= threshold
    bitl += 1 while n.anybits?(1 << bitl)
    "#{"~-" * (1 + (~n)[0, bitl])}(#{rbfuck_int((n >> bitl) + 1)}<<#{rbfuck_int(bitl)})"
  else
    bitl += 1 until n.anybits?(1 << bitl)
    "#{"-~" * n[0, bitl]}(#{rbfuck_int(n >> bitl)}<<#{rbfuck_int(bitl)})"
  end
end

def rbfuck_str(str)
  '""' + str.each_codepoint.map { |c| "<<#{rbfuck_int(c)}" }.join
end

code = "$><<(#{rbfuck_str("Hello, World!\n")})"
puts code
eval code

此程序便产生了本文开头的代码和输出。

why

预防也写了他自己的版本,记录如下。我没太看懂

$_={(:!)=>{}}
$__=[[]]<=>[]
$_[:-]=(->(_,__,*___,**____){(->(&__){__.(_,*___,**____)}).(&:"#{__}")})
$_[:%]=-~((-~-~$__)**-~$__)

$><<(""<<
  (~-~-$_[:%])**-~$__ + -~-~-~-~-~-~-~$__<<
  ($_[:%])**-~$__ + $__<<
  ($_[:%])**-~$__ + -~-~-~-~-~-~-~$__<<
  ($_[:%])**-~$__ + -~-~-~-~-~-~-~$__<<
  (-~$_[:%])**-~$__ + ~-~-~-~-~-~-~-~-~-~-~-$__<<
  (~-~-~-$_[:%])**-~$__ + ~-~-~-~-~-~-$__<<
  (~-~-~-~-$_[:%])**-~$__ + ~-~-~-~-~-$__<<
  (-~$_[:%])**-~$__ + ~-~-~-$__<<
  (-~$_[:%])**-~$__ + ~-~-~-~-~-~-~-~-~-~-~-$__<<
  (-~$_[:%])**-~$__ + ~-~-~-~-~-~-~-~-$__<<
  ($_[:%])**-~$__ + -~-~-~-~-~-~-~$__<<
  ($_[:%])**-~$__ + ~-$__<<
  (~-~-~-~-$_[:%])**-~$__ + ~-~-~-~-$__<<
  (~-~-~-~-$_[:%])**-~$__ + ~-~-~-~-~-$__
)

调试的过程中,我发现较旧版本的 Ruby 不支持对整数进行切片,遂写了如下 polyfill:

class Integer
  @@native_aref = Integer.instance_method(:[])
  @@omit = BasicObject.new
  def [](p1, p2 = @@omit)
    if @@omit.equal?(p2)
      if p1.is_a?(Range)
        if p1.begin.nil?
          return 0 if self.nobits?(~(-2 << p1.end))
          raise ArgumentError, "The beginless range for Integer#[] results in infinity"
        end

        return self >> p1.begin if p1.end.nil? or p1.end < p1.begin
        return self[p1.begin, p1.size]
      end

      return @@native_aref.bind(self).(p1)
    end

    # 为了在类型不对时报个错我容易吗(?
    @@native_aref.bind(self).(p1) unless p1.respond_to?(:to_int)
    @@native_aref.bind(self).(p2) unless p2.respond_to?(:to_int)

    self >> p1.to_int & ~(-1 << p2.to_int)
  end
end

fin


评论区

加载基于 GitHub issues 的 utteranc.es 评论区组件……