Skip to content

Latest commit

 

History

History
449 lines (367 loc) · 18.6 KB

04.logica.md

File metadata and controls

449 lines (367 loc) · 18.6 KB

Lógica

Estamos tratando con ordenadores. La computación se basa en bits. Los bits solo pueden tener dos valores: 0 o 1. Verdad o mentira. Este principio se utiliza para tomar decisiones como, por ejemplo, decidir si un número es mayor que otro. Ya nos preocuparemos de hacer una reedición de este libro cuando la computación cuántica introduzca nuevos paradigmas y sea asequible mortales como nosotros. Pero, vamos a centrarnos...

El tipo asociado a la verdad o mentira, se llama booleano en honor a George Boole y su famosa (y odiada por muchos) "álgebra de Boole".

En muchos lenguajes, podemos utilizar distintos tipos de datos para representar booleanos, En C y C++ podemos utilizar los enteros 0 y 1 como booleanos, aunque el propio C++ ya implemente estos tipos de forma nativa. En Python, cualquier cosa que pueda parecer un booleano lo es, como un entero o un conjunto. En Go eso no es así, siendo obligatorio el uso de variables de tipo booleano.

Operaciones lógicas

Las operaciones lógicas de las que disponemos en Go son las ya clásicas de otros lenguajes de programación. Al aplicar estas operaciones, obtendremos otra salida booleana.

Así pues, el operando negación, !, nos devolverá el valor contrario al que tengamos. De forma que !true tiene como valor false y !true devolverá true. Para utilizarlo, solo necesitas situarlo delante de lo que quieres negar. Para utilizarlo, solo se necesita un valor y, por tanto, es un operador unario.

El operador && corresponde con el operador lógico "and". Si algunos de los dos elementos que forman parte de la operación tiene como valor false, el resultado de la operación es false. Para que nos entendamos:

gore> true && true
true
gore> true && false
false
gore> false && true
false
gore> false && false
false

El último operador del que disponemos es ||, que corresponde al "or" lógico. Con que uno de los dos operados sea verdadero, el resultado también lo es:

gore> true && true
true
gore> true && false
true
gore> false && true
true
gore> false && false
false

Como ya se ha mencionado arriba, si intentas aplicar los operadores lógicos a elementos cuyo tipo no sea booleano, obtendremos un error:

gore> 1&&1
# command-line-arguments
/tmp/315015652/gore_session.go:16: invalid operation: 1 && 1 (operator && not defined on untyped number)
error: exit status 2
exit status 2
gore> int(1)&&int(1)
# command-line-arguments
/tmp/315015652/gore_session.go:16: invalid operation: int(1) && int(1) (operator && not defined on int)
error: exit status 2
exit status 2
gore> "falso"||"verdad"
# command-line-arguments
/tmp/315015652/gore_session.go:16: invalid operation: "falso" || "verdad" (operator || not defined on string)
error: exit status 2
exit status 2

Esto puede sernos bastante engorroso si venimos de, por ejemplo, Python. Pero también es una forma de ayudar al programador: solo se va a aplicar operaciones lógicas a aquellos elementos a los que haya que aplicárselo.

Ejercicio: se dice que una fórmula es "satisfacible" si es verdadera al menos para una combinación de valores. Con una fórmula de 3 variables, ¿cuantas combinaciones posibles de valores hay? Diseña una fórmula y pruébala sistemáticamente hasta que encuentres si es satisfacible o no. Altérala y conviértela en no-satisfacible, probándola también sistemáticamente.

Estos operadores necesitan dos operandos y, por tanto, se llaman binarios. En la mayoría de lenguajes de programación, o al menos en los más conocidos, se escriben de la misma forma.

Hay otras operaciones como , que corresponde con la implicación. Es decir, para A → B, si A, entonces B. Si A es cierto, entonces el resultado de A → B es B. Pero si es falso, el resultado estaría indeterminado, por lo que este operador es, en general, ternario, expresando dos posibles resultados dependiendo si el antecedente es cierto o falso. En Python lo expresaríamos como:

"Cierto" if True else "Falso"
"Cierto"

En Go no disponemos de operaciones ternarias en una línea, pero podemos obtener el mismo resultado con el siguiente código:

c := b
if a > b {
    c = a
}

Otras operaciones que devuelven valores lógicos

Algunas operaciones, de las que ya vienen predefinidas en el lenguaje, también nos devuelven booleanos. El ejemplo más claro, la comparación utilizando los operandos "<" (menor que), ">" (mayor que), "<=" (menor o igual que) o ">=" (mayor o igual que):

gore> 1 < 2
true
gore> -22 > -555
true
gore> -2 >= -53.3
true
gore> "arbol" < "brazo"
true
gore> "a" < 21.2
# command-line-arguments
/tmp/315015652/gore_session.go:16: cannot convert "a" to type float64
/tmp/315015652/gore_session.go:16: invalid operation: "a" < 21.2 (mismatched types string and float64)
error: exit status 2
exit status 2

El orden de los números es claro, mientras que las cadenas siguen el orden de la tabla Unicode. Es por ello que si comparamos, por ejemplo, con una ñ, empiezan a pasar cosas que pueden parecer "raras":

gore> "ñ" < "z"
false

Pero, como ya se ha dicho, el orden en el caso de las cadenas de texto viene determinado por la tabla Unicode.

De nuevo, solo podremos utilizar elementos del mismo tipo con estas operaciones.

También disponemos del operando "==" (igualdad). Este operando se utiliza de igual forma en casi todos los lenguajes, aunque algunos incluyen también el operando ""==="", como JavaScript, que diferencia entre igualdad abstracta e igualdad estricta y. ¿Por qué utilizar dos símbolos de igual en vez de uno para representar el concepto de "igualdad"? El operando "=" se guarda para la asignación de valores a variables, eso sí, una vez que han sido declaradas. Si no las declarado, tienes dos opciones: var i int = 1 o i:=1. La segunda no estará disponible si no estamos dentro de una función.

En cuanto a la desigualdad, la podemos representar utilizando "!=". Podríamos también negar la igualdad, es decir: !a == b en lugar de a != b , pero no es tan expresivo y sería correcto computacionalmente, pero no sintácticamente.

Ejercicio: Usando sólo los símbolos de mayor y menor y operadores lógicos (&& y ||), construir una expresión que sea equivalente a !=. Probar con diferentes valores que efectivamente
es así.

Ejercicio: probar, usando todos los casos posibles, las leyes de De Morgan. ¿Cuál sería el modo Go de expresar una expresión de ese estilo?

Precedencia

Al igual que en las operaciones matemáticas, la precedencia es muy importante en las operaciones lógicas. Por ejemplo: not 0 and 1, ¿a qué es igual? ¿A (not 0) and 1? ¿O a not( 0 and 1))? En general, la precedencia estará marcada por los paréntesis, luego la tendrán los operadores unarios (como - precediendo a un número) y finalmente los binarios, también por su orden. En los operadores lógicos, la precedencia es ! > && > ||, por lo que si quieres negar una expresión tendrás que ponerlo entre paréntesis.

Esto conviene tenerlo en cuenta cuando queremos crear operadores combinados, como el siguiente: En electrónica, conviene expresar las operaciones lógicas usando el mínimo número de conectores posibles. Y resulta que NAND, donde a NAND b == ! (a && b), es suficiente para expresar absolutamente todos los operadores lógicos unarios y binarios. En este caso el paréntesis hace que se ejecute primero el a && b antes que el !; la precedencia más alta del ! obliga a que se haga de esta forma.

Ejercicio: expresar and, or y not usando sólo NAND y probar exhaustivamente, usando valores de true y false, que es así.

Un poco de pensamiento computacional

Al final, los lenguajes de programación son lenguajes que te permiten expresar y resolver problemas. El pensamiento computacional no deja de ser pensar utilizando ordenadores para resolver un problema a través de un lenguaje de programación, cuya base radica en: descomposición, reconocimiento de patrones y abstracción.

La abstracción se basa, entre otras cosas, en saber elegir la estructura de datos adecuada para el problema con el que estamos trabajando. ¿Se trata de un número? ¿Un conjunto de números? ¿Un conjunto de caracteres? En cualquier lenguaje de programación, la estructura de datos elegida determina qué se puede hacer, eficientemente o posiblemente, con un dato determinado. Incluso el lenguaje de programación a usar, porque no todos los lenguajes pueden trabajar con todos los datos fácilmente. Por ejemplo, Python puede usar números complejos, con una parte real y otra imaginaria, como tipo de dato básico.

>>> 3+0.1j
3+0.1j
(3+0.1j)
>>> _**2
_**2
(8.99+0.6000000000000001j)

En Python, la parte imaginaria se representa, por alguna razón, por j en vez del convencional i, que es el que se usa en Ruby o en Perl6:

 :002 > (3+4i)*(5+0.1i)
 => (14.6+20.3i)

A priori, es complejo pensar en algún problema "del mundo real" que use los números complejos. Pero las matemáticas son también del mundo real, y problemas como "¿Diverge la sucesión (1+πi)ⁿ?" tendrá que comenzar por una fase de abstracción en la que se decidirá que la forma más eficiente de trabajar es usando estos números complejos. Trabajando en la línea de órdenes:

>>>1 + 3.14j

>>> _ * (1 + 3.14j)
_*(1+3.14j)
(3.141592653589793+9.864600932271951j)
>>> _ * (1 + 3.14j)
_*(1+3.14j)
(-27.833254273744135 + 19.729201864543903j)
>>> _ * (1 + 3.14j)
_*(1+3.14j)
(-89.78294812841199-67.66721655501269j)
>>> _ * (1 + 3.14j)
_*(1+3.14j)
(122.69211185432786-349.5856736782264j)

Vemos que tiene toda la pinta de diverger, así que la respuesta es que sí. El uso de números complejos venía de forma natural en este caso, y el hecho de que Python lo tenga como estructura de datos básica es un hecho afortunado, por lo que es la elección natural también. En otros lenguajes, como JavaScript, habría sido algo más complicado, teniendo que usar vectores y definir una serie de funciones de multiplicación de tales vectores al modo complejo.

En Go puedes trabajar de la misma forma con los números complejos, pero utilizando i para designar a la parte imaginaria.

La descomposición, por otro lado, trata de descomponer un problema monolítico en varios problemas más pequeños. Por ejemplo, descomponer un problema en una serie de operaciones básicas, todas ellas ya conocidas, que se puedan ejecutar en secuencia, en nuestro caso desde la línea de órdenes. Por ejemplo, veamos el problema de recrear todos los números del 1 al 5 usando exactamente 4 números 5, por separado o como cifras de un solo número; también como decimales. La primera descomposición es natural: hacer cada número por separado. Pero la segunda es tratar de conseguir partes de un número usando operaciones con 5s. Cero, por ejemplo, es 5-5. Uno, 5/5. Esta no es la solución a los dos primeros, pero sí para el tercero, 2 == 5/5 + 5/5. ¿Y cómo haríamos 5? Teniendo en cuenta que cualquier cosa multiplicada por 0 es cero...

Ejercicio: usa sólo cuatro números 5 para reconstruir los números del 0 al 5. Si puedes, hazlo hasta el 10.

El último mecanismo, el reconocimiento de patrones, se aleja más de lo puramente computacional, siendo simplemente una herramienta del pensamiento crítico. Pero eso no quiere decir que no sea útil. Por ejemplo, muchas de las técnicas, o la mayoría, de las usadas en el ejercicio anterior, ¿se podrían usar con cualquier otro número, es decir, sustituyendo el 5 por el 4 o por el 7?

Ejercicio: ¿Para qué números se pueden usar más o menos los mismos patrones que para el 5? ¿Funciona con el 4? ¿Funciona con el 6? Específicamente, ¿con qué resultados hay que hacer pequeños cambios para que funcione?

Como parte de la descomposición mencionada anteriormente está el hecho de que, eventualmente, toda la información que hay en un ordenador se reduce a puertas lógicas que pueden tomar un valor 0 o 1, verdad o mentira. Esto es lo que viene siendo trabajar con bits.

Operaciones a nivel de bit

Trabajar con números en su representación binaria muchas veces es la forma más rápida de hacer ciertas pruebas; dado que, de hecho, el número ya está en esa representación, hacer ciertas operaciones es más rápido que hacerlo de otra forma. Por ejemplo, los números pasados a binario tienen un 1 como último bit si son impares, y 0 si son pares. Para comprobar si este último bit está encendido (o sea, es un 1), se compara bit a bit con 1

gore> 33 & 1
1
gore> 30 & 1
0

& es el y bit a bit, o bitwise and, que toma cada bit de los dos operandos y lo compara, haciendo la operación lógica correspondiente y dando el resultado. De la misma forma se puede también multiplicar y dividir por 2 o cualquier potencia de 2, usando los operadores que corren a la izquierda << o derecha >> el número de bits que le digamos.

gore> 333 >> 1
166
gore> 888 << 1
1776

Así, << puede ser una forma rápida de hallar potencias de 2

gore> 2 << 36
137438953472

El siguiente operador es bastante interesante: XOR. Se trata de un operador lógico que es 1 cuando uno de los dos operandos es 1, pero no cuando lo son los dos, se denomina o exclusivo o exclusive or. No existe un símbolo en Go para representar este operando. Uno de sus usos es para hacer flipping, o cambiar el valor de un bit a su contrario. Una forma rápida de cambiar el bit n de un número puede ser la siguiente:

gore> 32 ^ (1 << 3)
40

1 << 3 correspondería al número binario 100, es decir, 1 corrido tres posiciones. 32 es el número binario 10000. 32 ^ 8 es 10000 ^ 100, es decir, cambiaría el tercer bit (empezando por izquierda o derecha, da igual), que convertiría el número en el 40 que vemos.

En otros lenguajes, como Lua, se usan exactamente los mismos símbolos para el mismo tipo de operadores. Estos operadores, al no ser naturales como la suma o la resta, se prestan a la creatividad por parte de los diseñadores de los lenguajes. Por ejemplo, en Lua hallaríamos la media de 8 y 32 de esta forma:

> (8+32) >> 1
20

que, casualmente es exactamente la misma forma en la que lo haríamos en Go o Python. El truco aquí es simplemente darse cuenta de que >> 1 es como una división entera por dos. Esto no funciona si alguno de los números es impar, pero si se trabaja sólo con enteros sería una forma muy rápida, ya que se hace directamente en el procesador usando instrucciones del mismo, de realizar esta operación.

Ejercicio Implementar el operando xor utilizando los demás operadores lógicos.

Es muy probable que para alguno de los ejercicios anteriores hayas tenido buscar en Internet. Si lo has hecho, te habrás dado cuenta de que

todo está en StackOverflow,

como ya hemos apuntado antes cuando hemos hablado de los problemas de entendimiento.

Para qué vamos a engañarnos, seguro que siguiendo este tutorial o, para el caso, cualquier otro, has consultado una o más veces Google buscando cómo hacer algo. Un dominio ligero del inglés y una conexión a Internet es lo que hace falta para aprender prácticamente cualquier cosa, desde lenguajes a macramé. A un nivel en el que se esté comenzando en un lenguaje, todas tus preguntas tendrán cumplida respuesta, incluso si provienes de otro lenguaje de programación preguntando equivalences of whatever thislanguage thisotherlanguage. En muchos casos habrá también vídeos o cursos enteros, en ocasiones gratuitos, para aprender lo que uno desee.

Muchas de las respuestas van a estar en StackOverflow, que se inició como un sitio de preguntas y respuestas pero que eventualmente ha evolucionado en una serie de sitios de diferentes temas, desde la historia hasta la estadística pasando por LaTeX o, por supuesto, programación.

StackOverflow tiene un sistema de puntuaciones por parte de los usuarios que presenta como mejor respuesta la mejor puntuada; eso te garantiza, más o menos, que tu consulta va a obtener una buena respuesta, si es que la hay, ya que además se pueden editar con el tiempo si quedan obsoletas por lo que sea. El único peligro puede ser que la respuesta esté obsoleta, pero de forma más o menos consistente sueles obtener una buena respuesta. A veces está en los foros de Reddit; Reddit tiene una serie de foros llamados subreddits donde se pueden postear preguntas y un sistema más o menos similar de puntuación.

Lo bueno que tienen estos sitios es precisamente esa interacción: puedes comenzar conversaciones y valorar las que hay. Si te das de alta puedes, y de hecho debes, votar a las preguntas que te ayuden, aparte de a las respuestas. También editarlas si tienes karma suficiente, y por supuesto si en alguna puedes ayudar, aportar soluciones. Eventualmente, puedes también preguntar si tienes alguna duda, no sin antes hacer lo siguiente:

  1. Buscar exhaustivamente por todo Google y el propio StackOverflow por una respuesta a esa pregunta y algunas más generales.

  2. Reducir al mínimo trozo de código el error o el problema que tengas. No puedes subir 200 líneas de código y esperar que alguien se las lea. Si el error está en 2 líneas, mucho mejor.

  3. Poner de forma precisa cuál es el error: mensaje de error completo o de forma precisa qué es lo que quieres hacer y no puedes. Durante este proceso el propio StackOverflow te sugerirá otras posibilidades. Léelas con cuidado, porque es posible que esté la respuesta.

  4. Una vez hecha la pregunta, difúndela por las redes sociales. Se hacen miles de preguntas en StackOverflow a cada rato. Una puede pasar sin pena, gloria ni solución. La difusión ayuda.

Una vez que empieces a hacerlo, te convertirás en un verdadero experto y comprenderás el chiste que circula diciendo que los cursos de programación se van a renombrar "Cortar y pegar de StackOverflow". Lo cierto es que ayuda mucho desde el proceso de aprendizaje... Hasta el proceso de aprendizaje, que en programación nunca acaba.

Concluyendo

Los operadores lógicos te serán necesarios a la hora de tomar decisiones sobre el flujo que tiene que seguir tu programa. Por otro lado, cuando vayas a enfrentarte a un problema, tienes que tomar algunas decisiones previas para tomar la mejor decisión posible. Por supuesto, es importante que aprendas a buscar información en Internet para resolver todos aquellos problemas ante los que te puedas encontrar mientras aprendes un lenguaje de programación.