Corte de matriz en Ruby: explicación del comportamiento ilógico (tomado de Rubykoans.com)

232

Estaba pasando por los ejercicios en Ruby Koans y me llamó la atención la siguiente peculiaridad de Ruby que encontré realmente inexplicable:

array = [:peanut, :butter, :and, :jelly]

array[0]     #=> :peanut    #OK!
array[0,1]   #=> [:peanut]  #OK!
array[0,2]   #=> [:peanut, :butter]  #OK!
array[0,0]   #=> []    #OK!
array[2]     #=> :and  #OK!
array[2,2]   #=> [:and, :jelly]  #OK!
array[2,20]  #=> [:and, :jelly]  #OK!
array[4]     #=> nil  #OK!
array[4,0]   #=> []   #HUH??  Why's that?
array[4,100] #=> []   #Still HUH, but consistent with previous one
array[5]     #=> nil  #consistent with array[4] #=> nil  
array[5,0]   #=> nil  #WOW.  Now I don't understand anything anymore...

Entonces, ¿por qué array[5,0]no es igual a array[4,0]? ¿Hay alguna razón por la matriz de corte en rodajas se comporta de esta extraña cuando se inicia en el (largo + 1) ª posición ??

Pascal Van Hecke
fuente
parece que el primer número es el índice para comenzar, el segundo número es cuántos elementos cortar
austin

Respuestas:

185

Rebanar e indexar son dos operaciones diferentes, y el problema radica en inferir el comportamiento de uno del otro.

El primer argumento en el segmento no identifica el elemento sino los lugares entre los elementos, definiendo los tramos (y no los elementos en sí):

  :peanut   :butter   :and   :jelly
0         1         2      3        4

4 todavía está dentro de la matriz, apenas; si solicita 0 elementos, obtiene el extremo vacío de la matriz. Pero no hay un índice 5, por lo que no puede cortar desde allí.

Cuando indexas (like array[4]), estás apuntando a los elementos mismos, por lo que los índices solo van de 0 a 3.

Amadan
fuente
8
Una buena suposición a menos que esto esté respaldado por la fuente. No siendo sarcástico, estaría interesado en un enlace, si es que lo hay, solo para explicar el "por qué", como preguntan el OP y otros comentaristas. Su diagrama tiene sentido, excepto que Array [4] es nulo. La matriz [3] es: gelatina. Esperaría que Array [4, N] sea nulo, pero es [] como dice el OP. Si es un lugar, es un lugar bastante inútil porque Array [4, -1] es nulo. Entonces no puedes hacer nada con Array [4].
Squarism
55
@squarism Acabo de recibir la confirmación de Charles Oliver Nutter (@headius en Twitter) de que esta es la explicación correcta. Es un gran desarrollador de JRuby, por lo que consideraría su palabra bastante autorizada.
Hank Gay el
18
La siguiente es la justificación de este comportamiento: blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Matt Briançon
44
Explicación correcta Debates similares sobre ruby-core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune
18
También se conoce como "publicación de cercas". El quinto poste de cerca (id 4) existe, pero el quinto elemento no. El corte es una operación de cercado, la indexación es una operación de elemento.
Matty K
27

esto tiene que ver con el hecho de que slice devuelve una matriz, documentación fuente relevante de Array # slice:

 *  call-seq:
 *     array[index]                -> obj      or nil
 *     array[start, length]        -> an_array or nil
 *     array[range]                -> an_array or nil
 *     array.slice(index)          -> obj      or nil
 *     array.slice(start, length)  -> an_array or nil
 *     array.slice(range)          -> an_array or nil

lo que me sugiere que si da el inicio que está fuera de los límites, devolverá nulo, por lo tanto, en su ejemplo, array[4,0]solicita el cuarto elemento que existe, pero pide que devuelva una matriz de cero elementos. Mientras que array[5,0]pide un índice fuera de límites, devuelve nulo. Esto quizás tenga más sentido si recuerda que el método de división está devolviendo una nueva matriz, sin alterar la estructura de datos original.

EDITAR:

Después de revisar los comentarios, decidí editar esta respuesta. Slice llama al siguiente fragmento de código cuando el valor arg es dos:

if (argc == 2) {
    if (SYMBOL_P(argv[0])) {
        rb_raise(rb_eTypeError, "Symbol as array index");
    }
    beg = NUM2LONG(argv[0]);
    len = NUM2LONG(argv[1]);
    if (beg < 0) {
        beg += RARRAY(ary)->len;
    }
    return rb_ary_subseq(ary, beg, len);
}

si mira en la array.cclase donde rb_ary_subseqse define el método, verá que devuelve nulo si la longitud está fuera de los límites, no el índice:

if (beg > RARRAY_LEN(ary)) return Qnil;

En este caso, esto es lo que sucede cuando se pasa 4, comprueba que hay 4 elementos y, por lo tanto, no activa el retorno nulo. Luego continúa y devuelve una matriz vacía si el segundo argumento se establece en cero. mientras que si se pasa 5, no hay 5 elementos en la matriz, por lo que devuelve cero antes de evaluar el argumento cero. código aquí en la línea 944.

Creo que esto es un error, o al menos impredecible y no el "Principio de la menor sorpresa". Cuando tenga unos minutos, al menos enviaré un parche de prueba fallido a ruby ​​core.

Jed Schneider
fuente
2
Pero ... el elemento indicado por el 4 en la matriz [4,0] tampoco existe ... - porque en realidad es el elemento 5the (conteo basado en 0, vea los ejemplos). Por lo tanto, también está fuera de los límites.
Pascal Van Hecke
1
tienes razón. Regresé y miré la fuente, y parece que el primer argumento se maneja dentro del código c como la longitud, no el índice. Editaré mi respuesta, para reflejar esto. Creo que esto podría presentarse como un error.
Jed Schneider
23

Al menos tenga en cuenta que el comportamiento es consistente. A partir de las 5, todo actúa igual; la rareza solo ocurre en [4,N].

Tal vez este patrón ayuda, o tal vez estoy cansado y no ayuda en absoluto.

array[0,4] => [:peanut, :butter, :and, :jelly]
array[1,3] => [:butter, :and, :jelly]
array[2,2] => [:and, :jelly]
array[3,1] => [:jelly]
array[4,0] => []

En [4,0], tomamos el final de la matriz. En realidad, me parecería bastante extraño, en lo que respecta a la belleza en los patrones, si el último regresara nil. Debido a un contexto como este, 4es una opción aceptable para el primer parámetro para que se pueda devolver la matriz vacía. Sin embargo, una vez que alcanzamos 5 y más, el método probablemente salga inmediatamente por naturaleza de estar totalmente y completamente fuera de los límites.

Matchu
fuente
12

Esto tiene sentido cuando considera que un segmento de matriz puede ser un valor l válido, no solo un valor r:

array = [:peanut, :butter, :and, :jelly]
# replace 0 elements starting at index 5 (insert at end or array):
array[4,0] = [:sandwich]
# replace 0 elements starting at index 0 (insert at head of array):
array[0,0] = [:make, :me, :a]
# array is [:make, :me, :a, :peanut, :butter, :and, :jelly, :sandwich]

# this is just like replacing existing elements:
array[3, 4] = [:grilled, :cheese]
# array is [:make, :me, :a, :grilled, :cheese, :sandwich]

Esto no sería posible si se array[4,0]devuelve en nillugar de []. Sin embargo, array[5,0]regresa nilporque está fuera de los límites (la inserción después del cuarto elemento de una matriz de 4 elementos es significativa, pero la inserción después del quinto elemento de una matriz de 4 elementos no lo es).

Lea la sintaxis de corte array[x,y]como "comenzando después de los xelementos array, seleccione hasta yelementos". Esto solo tiene sentido si arraytiene al menos xelementos.

Frank Szczerba
fuente
11

Esto tiene sentido

Debe poder asignar esas secciones, de modo que se definan de tal manera que el principio y el final de la cadena tengan expresiones de longitud cero que funcionen.

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
DigitalRoss
fuente
1
También puede asignar al rango ese segmento que devuelve cero, por lo que sería útil ampliar esta explicación. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas
¿Qué hace el segundo número al asignar? Parece ser ignorado. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee
@drewverlee no se ignora:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen
10

También encontré muy útil la explicación de Gary Wright. http://www.ruby-forum.com/topic/1393096#990065

La respuesta de Gary Wright es:

http://www.ruby-doc.org/core/classes/Array.html

Los documentos ciertamente podrían ser más claros, pero el comportamiento real es coherente y útil. Nota: estoy asumiendo la versión 1.9.X de String.

Ayuda a considerar la numeración de la siguiente manera:

  -4  -3  -2  -1    <-- numbering for single argument indexing
   0   1   2   3
 +---+---+---+---+
 | a | b | c | d |
 +---+---+---+---+
 0   1   2   3   4  <-- numbering for two argument indexing or start of range
-4  -3  -2  -1

El error común (y comprensible) es también asumir que la semántica del índice de argumento único es la misma que la semántica del primer argumento en el escenario (o rango) de dos argumentos. No son lo mismo en la práctica y la documentación no refleja esto. Sin embargo, el error definitivamente está en la documentación y no en la implementación:

argumento único: el índice representa una posición de un solo carácter dentro de la cadena. El resultado es la cadena de un solo carácter que se encuentra en el índice o nula porque no hay ningún carácter en el índice dado.

  s = ""
  s[0]    # nil because no character at that position

  s = "abcd"
  s[0]    # "a"
  s[-4]   # "a"
  s[-5]   # nil, no characters before the first one

dos argumentos enteros: los argumentos identifican una porción de la cadena para extraer o reemplazar. En particular, las partes de la cadena de ancho cero también se pueden identificar para que el texto se pueda insertar antes o después de los caracteres existentes, incluso en el frente o al final de la cadena. En este caso, el primer argumento no identifica una posición de carácter, sino que identifica el espacio entre caracteres como se muestra en el diagrama anterior. El segundo argumento es la longitud, que puede ser 0.

s = "abcd"   # each example below assumes s is reset to "abcd"

To insert text before 'a':   s[0,0] = "X"           #  "Xabcd"
To insert text after 'd':    s[4,0] = "Z"           #  "abcdZ"
To replace first two characters: s[0,2] = "AB"      #  "ABcd"
To replace last two characters:  s[-2,2] = "CD"     #  "abCD"
To replace middle two characters: s[1..3] = "XX"    #  "aXXd"

El comportamiento de un rango es bastante interesante. El punto de partida es el mismo que el primer argumento cuando se proporcionan dos argumentos (como se describió anteriormente), pero el punto final del rango puede ser la 'posición de caracteres' como con indexación simple o la "posición de borde" como con dos argumentos enteros. La diferencia está determinada por si se utiliza el rango de punto doble o el rango de punto triple:

s = "abcd"
s[1..1]           # "b"
s[1..1] = "X"     # "aXcd"

s[1...1]          # ""
s[1...1] = "X"    # "aXbcd", the range specifies a zero-width portion of
the string

s[1..3]           # "bcd"
s[1..3] = "X"     # "aX",  positions 1, 2, and 3 are replaced.

s[1...3]          # "bc"
s[1...3] = "X"    # "aXd", positions 1, 2, but not quite 3 are replaced.

Si revisa estos ejemplos e insiste y usa la semántica de índice único para los ejemplos de indexación doble o de rango, se confundirá. Tienes que usar la numeración alternativa que muestro en el diagrama ASCII para modelar el comportamiento real.

empuje
fuente
3
¿Puedes incluir la idea principal de ese hilo? (en caso de que el enlace algún día se vuelva inválido)
VonC
8

Estoy de acuerdo en que esto parece un comportamiento extraño, pero incluso la documentación oficialArray#slice muestra el mismo comportamiento que en su ejemplo, en los "casos especiales" a continuación:

   a = [ "a", "b", "c", "d", "e" ]
   a[2] +  a[0] + a[1]    #=> "cab"
   a[6]                   #=> nil
   a[1, 2]                #=> [ "b", "c" ]
   a[1..3]                #=> [ "b", "c", "d" ]
   a[4..7]                #=> [ "e" ]
   a[6..10]               #=> nil
   a[-3, 3]               #=> [ "c", "d", "e" ]
   # special cases
   a[5]                   #=> nil
   a[5, 1]                #=> []
   a[5..10]               #=> []

Desafortunadamente, incluso su descripción de Array#sliceno parece ofrecer ninguna idea de por qué funciona de esta manera:

Referencia de elemento: devuelve el elemento en el índice , o devuelve una submatriz que comienza en el inicio y continúa para elementos de longitud , o devuelve una submatriz especificada por rango . Los índices negativos cuentan hacia atrás desde el final de la matriz (-1 es el último elemento). Devuelve nil si el índice (o el índice inicial) están fuera de rango.

Mark Rushakoff
fuente
7

Una explicación proporcionada por Jim Weirich

Una forma de pensarlo es que la posición de índice 4 está en el borde de la matriz. Al pedir una porción, devuelve la mayor cantidad de la matriz que queda. Así que considere la matriz [2,10], la matriz [3,10] y la matriz [4,10] ... cada una devuelve los bits restantes del final de la matriz: 2 elementos, 1 elemento y 0 elementos respectivamente. Sin embargo, la posición 5 está claramente fuera de la matriz y no en el borde, por lo que la matriz [5,10] devuelve nulo.

suvankar
fuente
6

Considere la siguiente matriz:

>> array=["a","b","c"]
=> ["a", "b", "c"]

Puede insertar un elemento al principio (encabezado) de la matriz asignándolo a a[0,0]. Para poner el elemento entre "a"y "b", use a[1,0]. Básicamente, en la notación a[i,n], irepresenta un índice y nuna serie de elementos. Cuando n=0, define una posición entre los elementos de la matriz.

Ahora, si piensa en el final de la matriz, ¿cómo puede agregar un elemento a su final utilizando la notación descrita anteriormente? Simple, asigne el valor a a[3,0]. Esta es la cola de la matriz.

Entonces, si intenta acceder al elemento en a[3,0], obtendrá []. En este caso, todavía está en el rango de la matriz. Pero si intenta acceder a[4,0], obtendrá nilcomo valor de retorno, ya que ya no está dentro del rango de la matriz.

Lea más sobre esto en http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .

Tairone
fuente
0

tl; dr: en el código fuente, se invocan array.cdiferentes funciones dependiendo de si pasa 1 o 2 argumentos para dar como Array#sliceresultado valores de retorno inesperados.

(En primer lugar, me gustaría señalar que no codifico en C, pero he estado usando Ruby durante años. Entonces, si no está familiarizado con C, pero se toma unos minutos para familiarizarse con los conceptos básicos de funciones y variables realmente no es tan difícil seguir el código fuente de Ruby, como se demuestra a continuación. Esta respuesta se basa en Ruby v2.3, pero es más o menos lo mismo de vuelta a v1.9.)

Escenario 1

array.length == 4; array.slice(4) #=> nil

Si observa el código fuente de Array#slice( rb_ary_aref), verá que cuando solo se pasa un argumento ( líneas 1277-1289 ), rb_ary_entryse llama y pasa el valor del índice (que puede ser positivo o negativo).

rb_ary_entryluego calcula la posición del elemento solicitado desde el comienzo de la matriz (en otras palabras, si se pasa un índice negativo, calcula el equivalente positivo) y luego llama rb_ary_eltpara obtener el elemento solicitado.

Como se esperaba, rb_ary_eltregresa nilcuando la longitud de la matriz lenes menor o igual que el índice (aquí llamado offset).

1189:  if (offset < 0 || len <= offset) {
1190:    return Qnil;
1191:  } 

Escenario # 2

array.length == 4; array.slice(4, 0) #=> []

Sin embargo, cuando se pasan 2 argumentos (es decir , se llama al índice inicial begy la longitud del segmento len) rb_ary_subseq.

En rb_ary_subseq, si el índice inicial beges mayor que la longitud de la matriz alen, nilse devuelve:

1208:  long alen = RARRAY_LEN(ary);
1209:
1210:  if (beg > alen) return Qnil;

De lo contrario, lense calcula la longitud del segmento resultante y, si se determina que es cero, se devuelve una matriz vacía:

1213:  if (alen < len || alen < beg + len) {
1214:  len = alen - beg;
1215:  }
1216:  klass = rb_obj_class(ary);
1217:  if (len == 0) return ary_new(klass, 0);

Entonces, dado que el índice inicial de 4 no es mayor que array.length, se devuelve una matriz vacía en lugar del nilvalor que cabría esperar.

Pregunta contestada?

Si la pregunta real aquí no es "¿Qué código hace que esto suceda?", Sino más bien, "¿Por qué Matz lo hizo de esta manera?", Bueno, solo tendrás que comprarle una taza de café en el próximo RubyConf y preguntarle.

Scott Schupbach
fuente