miércoles, 30 de septiembre de 2015 0 comentarios

Predicates de manera fácil

De acuerdo a la documentación del .Net framework, el delegado genérico Predicate es algo que “representa un método que define un conjunto de criterios y determina si el objeto especificado cumple con esos criterios”. Pero ¿que quiere decir eso?. Leer la documentación que escriben los de Microsoft es fácil, pero no siempre se comprende todo lo que dice. Así que vamos a desmenuzar esa definición antes de mostrar algo de código para ver si se entiende mejor.

La definición dice que un Predicate “representa un método…”, y como los Predicates son un tipo de delegado, debemos entender que el Predicate es más bien un puntero a una función que realiza el trabajo.

Después dice que ese método al que apunta el Predicate “define un conjunto de criterios…” así que, la función contiene en su interior un conjunto de condiciones que se compararán con ciertas propiedades del objeto que se recibe como parámetro en la función.

Y para finalizar el análisis de la definición, ésta dice que “determina si el objeto especificado cumple con esos criterios”, es decir, que una vez que se hagan las comparaciones necesarias en el código interno de la función a la que apunta el Predicate, se debe informar si el objeto que se recibe como parámetro cumple o no con dichas condiciones. Es por eso que si se fijan, me he referido al método en estos párrafos como “función”, ya que se debe devolver un valor booleano que informe si el objeto que se está evaluando cumple o no con los criterios especificados dentro del método.

Pero ¿de qué objeto habla?, ¿que objeto es el que se recibe como parámetro?, ¿que objeto se evalúa?.

Más adelante veremos que cuando se pasa la dirección de la función que es utilizada como Predicate, en ningún momento se pasa algún argumento, entonces empiezan a aparecer las “cochinas” dudas y empieza uno a enredarse todito. Así que vamos aclarando eso del objeto que se recibe como parámetro de una vez.

Los delegados genéricos conocidos como Predicates, hasta donde yo sé, solo se pueden usar como argumentos en métodos de colecciones, listas, arrays, etc., es decir, en métodos de colecciones de objetos como por ejemplo Find, FindAll, FindLast, Exists, etc.. Por lo que es fácil deducir que los Predicates son como “callbacks”, es decir, funciones que se ejecutan y vuelven al punto donde fueron llamadas una y otra vez. No como funciones recursivas, sino que éstas son llamadas como en un ciclo foreach que se ejecuta tantas veces como elementos tenga la colección y que a su vez, al ir recorriendo dicha colección, va  pasando el elemento en turno como argumento a la función a la que apunta el Predicate para que revise si cumple con las condiciones en el interior de dicho método. Este bucle foreach del que hablo es creado cuando nuestro código es compilado y por lo tanto es prácticamente invisible para nosotros a menos que nos pongamos a visualizar el código IL que se genera cuando se compila nuestro programa.

Ahora bien, creo que es suficiente de tanta palabrería, así que veamos algo de código para terminar de entender todo este rollo que acabamos de leer. Así que supongamos que tenemos una clase Empleado definida de la siguiente forma:

  1. Public Class Empleado
  2.     Public Property Nombre As String
  3.     Public Property ApellidoPaterno As String
  4.     Public Property ApellidoMaterno As String
  5.     Public Property Edad As Byte
  6.     Public Property Salario As Decimal
  7.  
  8. End Class

Y que cargamos una lista con objetos de este tipo, por ejemplo:

  1. Sub CargaLista(ByVal lista As List(Of Empleado))
  2.     lista.Add(New Empleado() With {.Nombre = "Noé", .ApellidoPaterno = "García", .ApellidoMaterno = "Urías", .Edad = 30, .Salario = 15000.0})
  3.     lista.Add(New Empleado() With {.Nombre = "Enrique", .ApellidoPaterno = "Chávez", .ApellidoMaterno = "Castillo", .Edad = 42, .Salario = 16000.0})
  4.     lista.Add(New Empleado() With {.Nombre = "José", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López", .Edad = 50, .Salario = 10000.0})
  5.     lista.Add(New Empleado() With {.Nombre = "Julio César", .ApellidoPaterno = "Salazar", .ApellidoMaterno = "Lindoro", .Edad = 45, .Salario = 8000.0})
  6.     lista.Add(New Empleado() With {.Nombre = "Arturo", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López", .Edad = 37, .Salario = 12000.0})
  7. End Sub

Si tuviéramos que hacer búsquedas en esta lista utilizando por ejemplo el método Find de la misma, tendríamos que crear un delegado de tipo Predicate para identificar el elemento que coincide con la condición de búsqueda. He aquí una muestra:

  1. Private Function BuscaPorSalario(ByVal e As Empleado) As Boolean
  2.     If e.Salario = varSalario Then
  3.         Return True
  4.     Else
  5.         Return False
  6.     End If
  7. End Function

Y en el método Find pasar la dirección de la función que será utilizada como Predicate, por ejemplo:

  1. Dim oEmpleado As Empleado = lista.Find(AddressOf BuscaPorSalario)

Al pasar la dirección de memoria al método Find de la lista, el compilador crea un bucle interno en el código IL para utilizar la función de delegado BuscaPorSalario con cada elemento de la lista para ver si la condición de búsqueda se cumple. Cuando el salario de uno de los empleados en la lista coincida con el del valor de la variable varSalario, la función Predicate devolverá Verdadero y el método Find seleccionará ese objeto para ser asignado como resultado a la variable oEmpleado.^

Para finalizar voy a decir que este tipo de delegados es fácil de sustituír por expresiones lambda, ya que con ellas se reduce la cantidad de código y la legibilidad del mismo. Les recomiendo leer esta otra entrada del blog para entender mejor qué son y cómo se usan.

Y eso es todo, espero que en verdad haya sido fácil entender qué son y cómo funcionan los Predicates. Hasta pronto.

lunes, 28 de septiembre de 2015 0 comentarios

Expresiones Lambda de manera fácil

Para los que usan LINQ, las expresiones lambda son algo conocido ya que la mayor parte de los métodos de ese lenguaje usan delegados para permitirnos ordenar, filtrar, proyectar y realizar acciones sobre colecciones de objetos.

Pero… ¿qué es una expresión lambda?. Bueno, pues una Expresión Lambda es una función o subrutina con las siguientes características:

  • No tiene nombre o sea, es anónima
  • Puede contener una sola línea o múltiples líneas
  • No tiene modificadores de acceso (Public, Private, Overloads, etc.)
  • Su tipo de retorno es inferido, y
  • Se puede usar en cualquier lugar donde se permita usar un delegado, por ejemplo, en los métodos Find o FindAll de las listas genéricas.

Las expresiones lambda se incluyeron a partir de la versión 3.0 del .Net Framework como una especie de sustitución de los métodos anónimos que se podían usar con C# desde la versión 2.0 de dicho framework y que nunca estuvieron disponibles para VB y que servían para facilitar la declaración “inline” de un método usando la palabra reservada delegate.

Como las expresiones lambda son una manera más corta y concisa de escribir delegados, se pueden usar en cualquier parte donde los delegados sean admitidos así que, al igual que éstos, también pueden ser pasadas como argumentos a funciones y procedimientos.

Una expresión lambda puede ser asignada a una variable de la misma manera que se puede hacer con un delegado para usarse posteriormente, por ejemplo:

  1. Dim Suma = Function(numero1 As Integer, numero2 As Integer) numero1 + numero2 

En la asignación del ejemplo, se puede ver que la expresión lambda se asigna a una variable, la expresión lambda cumple con todas las características mencionadas al principio de este artículo, como que la función no tiene nombre, ejecuta una sola línea de código, no hay modificadores de acceso y no se especifica el tipo de dato de retorno del resultado.

Cabe mencionar que la expresión se pudo haber escrito en mas líneas si hubiera querido, por ejemplo:

  1. Dim Suma = Function(numero1 As Integer, numero2 As Integer)
  2.                Return numero1 + numero2
  3.            End Function

Solo que hay que colocar un End Function o un End Sub según se trate. En el caso de C# bastaría con encerrar las instrucciones de la expresión lambda entre llaves { } que son los especificadores de bloques de código de ese lenguaje de programación.

Ahora, para usar esa expresión lambda asignada a la variable “Suma”, bastaría hacer algo como lo siguiente para mostrar un número 3 en la consola de salida:

  1. Console.WriteLine(Suma(1, 2))

Pero se pueden preguntar ¿y que ventaja tienen entonces las expresiones lambda? eso también se puede hacer con una función común y corriente sin necesidad de usar una variable y también se puede hacer con un delegado. Pero una de las ventajas de usar expresiones lambda es que permiten simplificar el código y hacerlo todo como decimos en México “de un jalón” de la siguiente manera:

  1. Console.WriteLine((Function(numero1 As Integer, numero2 As Integer) numero1 + numero2)(1, 2))

Incluso, se puede omitir el tipo de datos de los parámetros que recibe la función, quedando como se ve a continuación:

  1. Console.WriteLine((Function(numero1, numero2) numero1 + numero2)(1, 2))

Y si aún así creen que esto no tiene nada de sorprendente ni de útil. Bueno, pues entonces vamos a ver un ejemplo que hará algo más complicado con la intención de esclarecer la utilidad y la conveniencia de las expresiones lambda.

Imaginemos tener una clase Empleado definida de la siguiente forma:

  1. Public Class Empleado
  2.     Public Property Nombre As String
  3.     Public Property ApellidoPaterno As String
  4.     Public Property ApellidoMaterno As String
  5.     Public Property Edad As Byte
  6.     Public Property Salario As Decimal
  7.  
  8. End Class

Y que cargamos una lista con objetos de este tipo, por ejemplo:

  1. Sub CargaLista(ByVal lista As List(Of Empleado))
  2.     lista.Add(New Empleado() With {.Nombre = "Noé", .ApellidoPaterno = "García", .ApellidoMaterno = "Urías", .Edad = 30, .Salario = 15000.0})
  3.     lista.Add(New Empleado() With {.Nombre = "Enrique", .ApellidoPaterno = "Chávez", .ApellidoMaterno = "Castillo", .Edad = 42, .Salario = 16000.0})
  4.     lista.Add(New Empleado() With {.Nombre = "José", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López", .Edad = 50, .Salario = 10000.0})
  5.     lista.Add(New Empleado() With {.Nombre = "Julio César", .ApellidoPaterno = "Salazar", .ApellidoMaterno = "Lindoro", .Edad = 45, .Salario = 8000.0})
  6.     lista.Add(New Empleado() With {.Nombre = "Arturo", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López", .Edad = 37, .Salario = 12000.0})
  7. End Sub

Si tuviéramos que hacer búsquedas en esta lista utilizando por ejemplo el método Find de la misma tendríamos que crear un delegado de tipo Predicate para identificar el elemento que coincide con la condición de búsqueda. He aquí una muestra:

  1. Private Function BuscaPorSalario(ByVal e As Empleado) As Boolean
  2.     If e.Salario = varSalario Then
  3.         Return True
  4.     Else
  5.         Return False
  6.     End If
  7. End Function

Y en el método Find pasar la dirección del Predicate como sigue:

  1. lista.Find(AddressOf BuscaPorSalario)

Al pasar la dirección de memoria al método Find de la lista, el compilador crea un bucle interno en el código IL para utilizar la función de delegado BuscaPorSalario con cada elemento de la lista para ver si la condición de búsqueda se cumple. Pero si quisiéramos buscar por medio de otra propiedad por ejemplo, su apellido paterno o su edad, tendríamos que escribir otro Predicate que identificara el elemento de acuerdo a esa otra propiedad, lo cual es una solución pero no es la ideal. En cambio, usando expresiones lambda, la tarea se simplifica muchísimo. Para demostrarlo, podemos hacer un método de prueba donde se verifica el resultado de algunas búsquedas en la lista, efectuadas utilizando expresiones lambda que usan diferentes propiedades de los elementos como a continuación se muestra:

  1. <TestMethod()>
  2. Sub Prueba()
  3.     Dim lista As List(Of Empleado) = New List(Of Empleado)
  4.     Dim Salario As Decimal = 10000D
  5.  
  6.     CargaLista(lista)
  7.  
  8.     Assert.AreEqual("José", (lista.Find(Function(e) e.Salario = Salario)).Nombre)
  9.     Assert.AreEqual("Noé", (lista.Find(Function(e) e.ApellidoPaterno = "García")).Nombre)
  10.     Assert.AreEqual("Enrique", (lista.Find(Function(e) e.Edad = 42)).Nombre)
  11.  
  12. End Sub 

Como se puede ver en las líneas 8, 9 y 10 del método de prueba, se verifican los resultados de las búsquedas en la lista de objetos empleado pero utilizando expresiones lambda para facilitar la búsqueda de dichos elementos en base a distintas propiedades de los mismos, reduciendo la cantidad de código y facilitando la lectura del mismo, además, si se mira con atención la línea 8, se puede ver como la expresión lambda utiliza la variable Salario declarada e inicializada en la línea 4 para identificar el elemento que cumple con la condición de búsqueda. Esto es posible con las expresiones lambda pero no con los delegados de tipo predicate, ya que las expresiones lambda tienen acceso a cualquier variable local y global visible en el contexto donde se encuentre escrita. En el caso de usar un predicate, la variable Salario tendría que ser visible a nivel de clase, formulario o global.

Ahora para finalizar, demostraré la utilidad de las expresiones lambda retomando el ejemplo de una entrada anterior donde hablaba sobre los delegados (a la que los remito si aún no la han leído) donde se terminó con un procedimiento genérico que implementaba el algoritmo de ordenamiento de burbuja que muestro a continuación:

  1. Public Sub Sort(Of T)(ByVal Lista As IList(Of T), ByVal Comparador As ComparerFunction(Of T))
  2.     Dim HuboCambio As Boolean
  3.  
  4.     Do
  5.         HuboCambio = False
  6.  
  7.         For i As Integer = 0 To Lista.Count - 1
  8.             If Comparador(Lista(i), Lista(i + 1)) > 0 Then
  9.                 Dim oTemp As T = Lista(i)
  10.                 Lista(i) = Lista(i + 1)
  11.                 Lista(i + 1) = oTemp
  12.                 HuboCambio = True
  13.             End If
  14.         Next
  15.  
  16.     Loop While (HuboCambio)
  17.  
  18. End Sub

donde se puede ver que se utiliza un delegado genérico para realizar la comparación entre los elementos de una lista. Por lo que, muestro también la declaración del delegado genérico:

  1. Public Delegate Function ComparerFunction(Of T)(ByVal a As T, ByVal b As T) As Integer

y del método compatible con el delegado genérico:

  1. Private Function ComparePersonByApellidos(ByVal a As Persona, ByVal b As Persona) As Integer
  2.    Return String.Format("{0} {1}", a.ApellidoPaterno, a.ApellidoMaterno).CompareTo(String.Format("{0} {1}", b.ApellidoPaterno, b.ApellidoMaterno))
  3. End Function

Y también un mostraré el método de prueba de estos pequeños trozos de código:

  1. Imports System.Text
  2. Imports DelegatesAndLambdas
  3.  
  4. <TestClass()>
  5. Public Class BubbleSorterTest
  6.  
  7.     <TestMethod()>
  8.     Public Sub CreateAndSortAList()
  9.         Dim lista As List(Of Persona) = New List(Of Persona)
  10.  
  11.         lista.Add(New Persona() With {.Nombre = "Noé", .ApellidoPaterno = "García", .ApellidoMaterno = "Urías"})
  12.         lista.Add(New Persona() With {.Nombre = "Enrique", .ApellidoPaterno = "Chávez", .ApellidoMaterno = "Castillo"})
  13.         lista.Add(New Persona() With {.Nombre = "José", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López"})
  14.         lista.Add(New Persona() With {.Nombre = "Julio César", .ApellidoPaterno = "Salazar", .ApellidoMaterno = "Lindoro"})
  15.         lista.Add(New Persona() With {.Nombre = "Arturo", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López"})
  16.  
  17.         Dim callback As ComparerFunction(Of Persona) = New ComparerFunction(Of Persona)(AddressOf CompareByApellidos)
  18.  
  19.         Call New BubbleSorter().Sort(lista, callback)
  20.  
  21.         Assert.AreEqual("García", lista(3).ApellidoPaterno)
  22.         Assert.AreEqual("Lindoro", lista(4).ApellidoMaterno)
  23.         Assert.AreEqual("Enrique", lista(0).Nombre)
  24.  
  25.     End Sub
  26.  
  27.     Public Function CompareByApellidos(ByVal a As Persona, ByVal b As Persona) As Integer
  28.         Return String.Format("{0} {1}", a.ApellidoPaterno, a.ApellidoMaterno).CompareTo(String.Format("{0} {1}", b.ApellidoPaterno, b.ApellidoMaterno))
  29.     End Function
  30.  
  31. End Class

Ahora, mostraré como se puede simplificar la escritura de este método con la utilización de Expresiones lambda para que vean que no solo sirven para hacer búsquedas.

Si vemos con atención el método de prueba en el bloque de código anterior, cuando el delegado es creado se le pasa la dirección de memoria de la función compatible que es CompareByApellidos la cual recibe un par de parámetros de tipo Persona.  Todo esto está bien, pero para que escribir tanto si se puede hacer de la siguiente manera:

  1. <TestClass()> _
  2. Public Class BubbleSorterWithLambdaExpresionTest
  3.  
  4.     <TestMethod()>
  5.     Public Sub CreateAndSortAList()
  6.         Dim lista As List(Of Persona) = New List(Of Persona)
  7.  
  8.         lista.Add(New Persona() With {.Nombre = "Noé", .ApellidoPaterno = "García", .ApellidoMaterno = "Urías"})
  9.         lista.Add(New Persona() With {.Nombre = "Enrique", .ApellidoPaterno = "Chávez", .ApellidoMaterno = "Castillo"})
  10.         lista.Add(New Persona() With {.Nombre = "José", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López"})
  11.         lista.Add(New Persona() With {.Nombre = "Julio César", .ApellidoPaterno = "Salazar", .ApellidoMaterno = "Lindoro"})
  12.         lista.Add(New Persona() With {.Nombre = "Arturo", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López"})
  13.  
  14.         Call New BubbleSorter().Sort(lista, _
  15.         Function(a, b) String.Format("{0} {1}", a.ApellidoPaterno, a.ApellidoMaterno).CompareTo(String.Format("{0} {1}", b.ApellidoPaterno, b.ApellidoMaterno)))
  16.  
  17.         Assert.AreEqual("García", lista(3).ApellidoPaterno)
  18.         Assert.AreEqual("Lindoro", lista(4).ApellidoMaterno)
  19.         Assert.AreEqual("Enrique", lista(0).Nombre)
  20.  
  21.     End Sub 
  22.  
  23. End Class

Como podemos observar, las líneas de código son menos (un 30% menos aproximadamente), el código es más simple (una vez que te acostumbras a las expresiones lambda) y más legible. Además se resta bastante la complejidad al dejar de utilizar los delegados que funcionan como intermediarios. Aquí todo es más directo. Lo interesante de este fragmento de código ocurre en las líneas 14 y 15 que en realidad son una sola línea pero la dividí por cuestiones de espacio en el blog.

Analizando este par de líneas podemos ver que ya no fue necesario declarar ningún delegado ni la función CompareByApellidos. En su lugar se utilizó una expresión lambda, la cual cumple con todas las características mencionadas al inicio de este artículo al igual que todas las que se usaron en los ejemplos anteriores.

Los parámetros recibidos “a” y “b” son 2 elementos de la lista diferentes pero subsecuentes y el compilador los va remplazando mediante un bucle que crea cuando compilamos el código por lo que nunca lo vemos.

Y bueno… creo que esto es todo. Espero que si llegaron a leer estas últimas líneas consideren que el título de esta entrega fue el correcto.

 
;