Nim: Как динамически определить срез, который может быть либо вперед, либо назад?

#types #slice #nim-lang

#типы #срез #nim-lang

Вопрос:

Я хотел бы динамически определить 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

Мой вариант использования более сложный, но он сводится к программе командной строки, которая принимает в качестве аргументов входной текст, «штрих-код» и начальную позицию штрих-кода, и сообщает, присутствует ли штрих-код во входном тексте в указанной позиции. Если позиция задана как отрицательное значение 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" amp; 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 объект, который я могу повторно использовать вместо повторного вычисления фрагмента «на месте».

Комментарии:

1. Можете ли вы привести примеры по API, которые вы пытаетесь достичь (мне не ясно из опубликованных примеров)?

2. @pietroppeter Извините за мою запоздалую реакцию. Я добавил пример API, которого я хотел бы достичь (скорее: которого я действительно достигаю, но я хотел бы «оптимизировать» внутреннюю реализацию).

3. Можете ли вы просто использовать объект HSlice? пусть mySlice = (bcStart-1)..<(bcStart bcLen — 1)

4. Я не думаю, что здесь можно использовать общий тип Slice [T] (или HSlide [T, U]), поскольку во время компиляции он не будет знать, является ли T int или BackwardsIndex . ниже я обновляю свой ответ, утверждая, что следует просто избегать мышления в терминах срезов.

Ответ №1:

Все проблемы связаны с тем фактом, что ваш тип является классом типов. Это псевдотип, который может использоваться только во время компиляции в качестве параметра для перегрузки процесса (или для is оператора). В частности, его нельзя назначить var (первая ошибка, о которой вы сообщаете), и его нельзя динамически использовать во время выполнения.

Остальные 2 ошибки, которые вы получаете, связаны с 1) тем фактом, что s1 не определено за пределами области if. 2) Тот факт, что компилятору требуется уникальный тип для s1 (сначала он выводит тип из предложения if, а затем применяет для предложения else).

Варианты объектов (также типы сумм, алгебраические типы данных в Nim; терминологический тип объединения не часто используется в 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_start и barcode должны быть сопоставлены несколько раз с разными текстами (возможно, переменной длины)
  • лучше избегать вычисления «фрагмента» строки, поскольку он выделяет новую строку (см. Здесь). Я подозреваю, что это большее улучшение производительности, чем предварительное вычисление начального индекса.
  • по предыдущим двум пунктам объект, который должен быть «скомпилирован» перед многократным применением сопоставления, на самом деле не является фрагментом (отсюда и название myPattern)

Комментарии:

1. Спасибо за полезные ответы. На самом деле я бы сказал, что проблема YXZ: мое упрощение не совсем точно отражает мой реальный вариант использования. Простое сопоставление было чрезмерным упрощением, о котором я подумал, чтобы иметь что-то, что могло бы «оправдать» извлечение фрагмента, но в моем реальном случае использования, после извлечения фрагмента текста с помощью фрагмента, я делаю больше, чем просто проверяю его соответствие штрих-коду. Я передаю его функции, которая, например, вычисляет расстояние до штрих-кода, поэтому мне нужен доступ к полному фрагменту текста.

2. Но, возможно, я могу реализовать все, что хочу, используя (полные) циклы для исходного текста, если это имеет хорошие шансы быть более эффективным, чем выделение новой строки.

Ответ №2:

выражение

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

не удается скомпилировать на статически типизированном языке, поэтому вам нужно вставить 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] #  ""
  

Редактировать
Вы хотели иметь возможность передавать фрагмент, который можно было бы использовать в разных входных строках. Вот как это будет выглядеть с учетом вышеизложенного:

 #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"
  

Комментарии:

1. Если конечные строки соответствуют типу API, которого хотел достичь OP, я не вижу, в чем было бы преимущество перед стандартным синтаксисом (например echo[^5 .. 9] , все, кроме последней строки, будут скомпилированы нормально). Хороший ответ, хотя 🙂

2. О, я активно не рекомендую его использовать. но, надеюсь, это показывает, что добавление желаемого синтаксиса в Nim довольно элегантно.