Nim:如何动态定义可以向前或向后的 Slice?

Nim: How to dynamically define a Slice that can be either forwards of backwards?

我想动态定义一个 Slice,它可以基于前向或后向索引(取决于它的起始位置是正数还是负数)。

我正在尝试 https://play.nim-lang.org/

我尝试了一个联合类型如下:

type mySlice = Slice[BackwardsIndex] | Slice[int]
var sl: mySlice
let s = "1234567890"
let bcStart = 3
let bcLen = 3
if bcLen < 0:
  sl = (bcStart-1)..<(bcStart+bcLen-1)
else:
  sl = ^(bcStart+bcLen-1)..^(bcStart)
echo s[sl]

失败 /usercode/in.nim(2, 5) Error: invalid type: 'mySlice' for var

我试过了

let s = "1234567890"
let bcStart = 3
let bcLen = 3
if bcLen < 0:
  let sl = (bcStart-1)..<(bcStart+bcLen-1)
else:
  let sl = ^(bcStart+bcLen-1)..^(bcStart)
echo s[sl]

失败如下:

/usercode/in.nim(5, 7) Hint: 'sl' is declared but not used [XDeclaredButNotUsed]
/usercode/in.nim(7, 7) Hint: 'sl' is declared but not used [XDeclaredButNotUsed]
/usercode/in.nim(8, 8) Error: undeclared identifier: 'sl'

我还尝试了以下方法:

let s = "1234567890"
let bcStart = 3
let bcLen = 3
let sl =
  if bcLen < 0:
    (bcStart-1)..<(bcStart+bcLen-1)
  else:
    ^(bcStart+bcLen-1)..^(bcStart)
echo s[sl]

还有一种不同的失败方式:

/usercode/in.nim(8, 23) Error: type mismatch: got <HSlice[system.BackwardsIndex, system.BackwardsIndex]> but expected 'HSlice[system.int, system.int]'

为什么会出现那些故障,我应该怎么办?

编辑 (09/09/2020) 需要 API

我的用例比那个更复杂,但它相当于一个命令行程序,它将输入文本、“条形码”和条形码起始位置作为参数,并告诉它条形码是否存在于指定位置的输入文本。如果position给出的是负int,表示我们指定的是从末尾开始的位置。

我有一些东西按预期工作:

$ cat src/test.nim
import docopt
from strutils import parseInt

# https://github.com/docopt/docopt.nim
const doc = """

Usage:
  test -t <input_text> -b <barcode> -s <barcode_start>

-h --help                                 Show this help message and exit.
-t --input_text <input_text>              Text in which to search for the barcode.
-b --barcode <barcode>                    Barcode to search.
-s --barcode_start <barcode_start>        Position at which the barcode starts (1-based), negative if from end.
"""

proc match_text(inText: string, barcode: string, bcStart: int): bool =
  var
    bcSeq: string
    bcLen: int = barcode.len
  if bcStart < 0:
    bcSeq = inText[^(bcLen - bcStart - 1)..^(-bcStart)]
  else:
    bcSeq = inText[(bcStart-1)..<(bcStart + bcLen - 1)]
  if bcSeq == barcode:
    result = true
  else:
    result = false

when isMainModule:
  let args = docopt(doc)
  var
    barcode: string
    inText: string
    bcStart: int
  for opt, val in args.pairs():
    case opt
    of "-t", "--input_text":
      inText = $args[opt]
    of "-b", "--barcode":
      barcode = $args[opt]
    of "-s", "--barcode_start":
      bcStart = parseInt($val)
    else:
      echo "Unknown option" & opt
      quit(QuitFailure)
  if match_text(inText, barcode, bcStart):
    echo "Matches"
  else:
    echo "Doesn't match"

建筑工程:

$ nimble build
# [successful build output]

测试工作:

$ ./bin/test -t aacgttb -b aa -s 1
Matches
$ ./bin/test -t aacgttb -b aa -s 2
Doesn't match
$ ./bin/test -t aacgttb -b tt -s -1
Doesn't match
$ ./bin/test -t aacgttb -b tt -s -2
Matches

但是,在我的实际应用程序中,我在不同的文本片段中多次重复使用相同的切片,所以我想定义一个 Slice 对象,我可以重复使用而不是重复计算切片“就地”.

这些问题都与您的类型是 Type Class 这一事实有关。这是一个伪类型,只能在编译时用作 proc 重载(或 is 运算符)的参数。特别是它不能分配给 var(您报告的第一个错误)并且它不能在 运行 时间动态使用。

您得到的其他 2 个错误是由于 1) s1 未在 if 范围之外定义的事实。 2) 编译器想要 s1 的唯一类型这一事实(它从第一个 if 推断类型,然后对 else 子句强制执行)。

Object variants (also Sum types, Algebraic Data types in Nim; terminology Union Type 在 Nim 中不常使用)通常是在 Nim 中实现动态类型最直接的方式(经典示例是 JsonNode)。

编辑:根据需要 API

由于重点是“切片”的可重用性和性能改进,因此可能会使用以下内容(也在这里:https://play.nim-lang.org/#ix=2wXp):

type myPattern = object
  barcode: string
  start: int
  isBackwards: bool

proc initMyPattern(barcode: string, bcStart: int): myPattern =
  # no need to have a new variable for barcode.len since it is already available (not computed) for a string
  # also no need to precompute last index of slice because it will not be used
  if bcStart < 0:
    myPattern(barcode: barcode, start: barcode.len - bcStart - 1, isBackwards: true)
  else:
    myPattern(barcode: barcode, start: bcStart - 1, isBackwards: false)


proc startIndex(inText: string, p: myPattern): int =
  if p.isBackwards:
    # this cannot be precomputed if len of inText is variable
    inText.len - p.start
  else:
    p.start
   
proc match(inText: string, p: myPattern): bool =
  var
    i =  startIndex(inText, p)
    j = 0
  # case where inText is not long enough to match
  if i + p.barcode.len - 1 >= inText.len:
    return false
  # instead of computing the slice of inText (which allocates a new string), we directly iterate over indices
  while j < p.barcode.len:
    if p.barcode[j] != inText[i]:
      return false
    inc i
    inc j
  return true

assert "aacgttb".match initMyPattern("aa", 1)
assert not "aacgttb".match initMyPattern("aa", 2)
assert not "aacgttb".match initMyPattern("tt", -1)
assert "aacgttb".match initMyPattern("tt", -2)
assert not "aacgttb".match initMyPattern("ttbb", -2)
echo "tests successful"

备注:

  • 我假设固定的 barcode_startbarcode 需要针对不同的文本(可能长度可变)进行多次匹配
  • 最好避免计算字符串的“切片”,因为它会分配一个新字符串(请参阅 here)。我怀疑这比开始索引的预计算有更大的性能改进。
  • 根据前两点,在多次应用 match 之前要“编译”的对象并不是真正的 Slice(因此得名 myPattern)

表达式

let sl = if (bcLen >0): bcLen else: BackwardsIndex(bcLen)#Error: type mismatch!

无法在静态类型语言中编译,因此您需要使用继承或 Variant

来封装 sl

制作切片的时候再开箱。你可以这样做:

type
  PosOrNegKind = enum
    Pos,Neg
  PosOrNeg = object
    case kind:PosOrNegKind
    of Pos: posVal:int
    of Neg: negVal:int
  mySlice = object
    beg,fin:PosOrNeg

proc `[]`(str:string,sl:mySlice):string =
  let beg = case sl.beg.kind
    of Pos: sl.beg.posVal
    of Neg: len(str) + sl.beg.negVal
  let fin = case sl.fin.kind
    of Pos: sl.fin.posVal
    of Neg: len(str) + sl.fin.negVal
  str[beg .. fin]

proc posOrNeg(x:int):PosOrNeg =
  if (x >= 0): PosOrNeg(kind: Pos, posVal: x)
  else:       PosOrNeg(kind: Neg, negVal: x)

proc createSlice(beg,fin:int):mySlice =
  result.beg = posOrNeg(beg)
  result.fin = posOrNeg(fin)

let sl = createSlice(3,-3)
echo s[sl]# "34567"

但是对于这个特定的用例,你在值本身中有一个自然的鉴别器(无论 int 是正数还是负数)所以你可以这样做:

type
  MySlice = object
    a,b:int

proc `--`(a,b:int):MySlice = MySlice(a: a, b: b)

proc `[]`(s:string,m:MySlice):string =
  var beg = if (m.a < 0): s.len + m.a else: m.a 
  var fin = if (m.b < 0): s.len + m.b else: m.b
  
  #safety checks
  if fin < beg: return ""
  if fin >= s.len: fin = s.len - 1
  if beg < 0: beg = 0

  s[beg..fin]
  
echo s[3 -- 5] #  "345"
echo s[3 -- -2] # "345678"
echo s[-5 -- 9] # "56789"
echo s[-8 -- -2] # "2345678"
echo s[-1 -- 1] #  ""

编辑 您希望能够传递可用于不同输入字符串的 Slice。以下是上面的内容:

#fixing off-by-one errors left as an exercise for the reader 
proc make_slice(barcode:string,bcStart:int):mySlice=
  let bcLen = barcode.len
  if bcStart < 0:
    (bcStart - bcLen) -- bcStart
  else:
    bcStart -- (bcStart + bcLen)

let sl = make_slice("abaca", -3)
for inText in @["abacus","abacadacaba","abracadabra"]:
  if inText[sl] == barcode:
    echo "matches"