viernes, 10 de marzo de 2023

Recursos sobre scala


 Queres empezar en scala y no sabes de donde sacar info? 

Te paso una lista de recursos: 

Platforms

Community

Coding Tools:

Programming Environments

Build Tools

Code Formatting / Linting

Free Books, Tutorials and Guides:

Non-free Books:

Advanced!:

Free Scala Courses: * Functional Programming Principles in Scala
Functional Program Design in Scala
Parallel Programming * Big Data Analysis with Scala and Spark
Introduction to Programming with Dependent Types in Scala (advanced)

Non-Free Courses:

Scala Conferences: * Functional Scala (UK/Remote)
LambdaConf (USA) * Typelevel Summits (Misc.) * Scala by the Bay (USA) * flatMap (Norway) * Scala Up North (Canada) * Scala Days (USA, Europe)
Scala World (UK)
Scala Swarm (Portugal)
Scala.io (France)
Scalar (Central Europe) * Scala Sphere (Poland)
nescala (USA)
LX SCALA (South-West Europe)
ScalaConf (Russia)

Podcasts:

Scala Jobs:

Scala Libraries:

Web Development and Microservices
ZIO HTTP * Caliban (https://github.com/ghostdogpr/caliban) * Play
Akka HTTP
Lagom * Sttp (HTTP Client) * http4s * Finch * Udash - Frontend and Backend
Lift
Scalatra
Skinny
Vert.x
Sangria - GraphQL

Web Front End (Scala.js)

Database Access

Functional Programming

Concurrency / Parallelism

Mathematics

Distributed Computing

Blockchain

Miscellaneous:

Open Source Applications written in Scala

Related Communities:

Blogs/Periodicals:

jueves, 9 de marzo de 2023

Gleam, un lenguaje funcional que corre en la Vm de Erlang.


Gleam es un lenguaje funcional que corre sobre la maquina virtual de Erlang. Es de tipado estatico, funcional y sintaxis similar a Erlang y Elixir.   

Gleam se ejecuta en la máquina virtual Erlang que impulsa sistemas a escala planetaria como WhatsApp y Ericsson, está listo para cargas de trabajo de cualquier tamaño. Gracias a un sistema de simultaneidad basado en múltiples núcleos que puede ejecutar millones de tareas simultáneas, estructuras de datos rápidas e inmutables y un recolector de basura simultáneo, su servicio puede escalar y mantenerse con facilidad.

Gleam viene con compilador, herramienta de compilación, formateador, integraciones de editor y administrador de paquetes, todo integrado, por lo que crear un proyecto Gleam es simplemente ejecutar gleam new.

Como parte del ecosistema BEAM más amplio, los programas Gleam pueden usar miles de paquetes publicados, ya sea que estén escritos en Gleam, Erlang o Elixir.

Sin valores nulos, sin excepciones, mensajes de error claros y un sistema de tipo práctico. Ya sea que esté escribiendo código nuevo o manteniendo código antiguo, Gleam está diseñado para hacer que su trabajo sea lo más divertido y libre de estrés posible.

Gleam también puede compilar en JavaScript, lo que le permite usar su código en el navegador o en cualquier otro lugar donde se pueda ejecutar JavaScript. También genera definiciones de TypeScript, por lo que puede interactuar con su código Gleam con confianza, incluso desde el exterior.


Veamos un pequeño hola mundo hecho en Gleam:

import gleam/io


pub fn main() {

  io.println("hello, friend!")

}


Por ejemplo veamos código asincrono: 


fn spawn_task(i) {

  task.async(fn() {

    let n = int.to_string(i)

    io.println("Hello from " <> n)

  })

}


pub fn main() {

  // Run a million threads, no problem

    list.range(0, 1_000_000)

  |> list.map(spawn_task)

  |> list.each(task.await_forever)

}

Dejo link: https://gleam.run/

martes, 7 de marzo de 2023

¿Tu empresa está lista para dejar que los datos hagan el trabajo pesado?

 

domingo, 5 de marzo de 2023

Primeros pasos con Phoenix


Antes de empezar de manera muy rápida aclaremos que Phoenix es un framework web o para hacer API en Elixir. 

Phoenix está escrito en Elixir, y nuestro código de aplicación también estará escrito en Elixir. Por ende el primer paso es instalar Elixir , en mi caso lo voy a instalar con asdf

asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git

asdf install elixir 1.14.3-otp-25

El código de Elixir se compila en el código de bytes de Erlang para ejecutarse en la máquina virtual de Erlang. Sin Erlang, el código de Elixir no tiene una máquina virtual para ejecutarse, por lo que también debemos instalar Erlang. 

asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git

asdf install erlang 25.0.3

Y ahora vamos a setear las versiones: 

asdf global erlang 25.0.3

asdf global elixir 1.14.3-otp-25

Pero pueden ver como instalar en su equipo en el siguiente link : https://elixir-lang.org/install.html

Cuando instalamos Elixir siguiendo las instrucciones de la página de instalación de Elixir, normalmente también obtendremos Erlang. Para checkear esto hacemos : 

emanuel@crespo:~$ elixir --version

Erlang/OTP 25 [erts-13.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]


Elixir 1.14.3 (compiled with Erlang/OTP 25)


También podemos escribir erl que es el comando para la relp de Erlang. 

Si acabamos de instalar Elixir por primera vez, también necesitaremos instalar el administrador de paquetes Hex. Hex es necesario para ejecutar una aplicación de Phoenix (mediante la instalación de dependencias) y para instalar cualquier dependencia adicional que podamos necesitar en el camino. En mi caso es : 

mix local.hex


Ahora debemos instalar phoenix, en mi caso voy a instalar la ultima versión

mix archive.install hex phx_new 

Por lo visto, ya esta todo instalado, ahora vamos a crear nuestro primer proyecto: 

mix phx.new hello_world --module HelloWorld

Y si todo salio bien vamos a ejecutarlo: 

cd hello_world/
mix phx.server

Si vamos a http://localhost:4000/ nos aparecerá una pagina. 

Van a ver que tira como 5000 errores porque no configuramos la base de datos, pero eso lo vamos hacer en el proximo post. 

Dejo link; https://www.phoenixframework.org/

viernes, 3 de marzo de 2023

Asdf, multiples sdks en una sola aplicación

 


Asdf, es como sdkman pero para multiples plataformas y lenguajes. Con Asdf puedo instalar node, elixir, python, etc... 

Para instalarlo en linux tenemos que tener git y curl instalados : 


apt install curl git


Usamos git para bajar le código: 

git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.11.2


Luego ejecutamos lo siguiente: 

. "$HOME/.asdf/asdf.sh"

. "$HOME/.asdf/completions/asdf.bash"


Y ya esta!! 


Ahora instalemos nodejs por ejemplo, primero instalamos el plugin para la plataforma: 

asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git


y instalamos node : 

$ asdf install nodejs latest

Trying to update node-build... ok

Downloading node-v19.7.0-linux-x64.tar.gz...

-> https://nodejs.org/dist/v19.7.0/node-v19.7.0-linux-x64.tar.gz

Installing node-v19.7.0-linux-x64...

Installed node-v19.7.0-linux-x64 to /home/emanuel/.asdf/installs/nodejs/19.7.0

En mi caso instalo la versión 19.7.0, ahora tengo que setearla : 

asdf local nodejs 19.7.0

con "asdf list nodejs" puedo saber las versiones que tengo instaladas. 

Y listo!


Dejo link: 



miércoles, 1 de marzo de 2023

The best Golang Learning Resources on the Web


Quiero recomentarles el sitio golang resources que contiene un conjunto de recursos clasificados por nivel de conocimiento. Es muy visual y muy completa. Sin más....

Dejo link: https://golangresources.com/

lunes, 27 de febrero de 2023

Inyección de dependencia en Go usando Fx parte 2

 


Seguimos con fx, ahora vamos a hacer otro ejemplo con fx y Go basados en este ejemplo hecho con spring y java.

Primero vamos a hacer una interfaz que nos permita obtener un saludo : 

type Saludor interface {

GetSaludo() string

}

Luego vamos a implementarla en español e ingles : 


type SaludorEs struct {

}

func (saludoEs SaludorEs) GetSaludo() string {

return "Hola !!"

}


type SaludorEn struct {
}

func (saludoEn SaludorEn) GetSaludo() string {
return "Hi !!"
}

Y vamos a hacer métodos que nos permitan crear estas implementaciones : 

func CrearSaludadorEs() (Saludor, error) {
return SaludorEs{}, nil
}

func CrearSaludadorEn() (Saludor, error) {
return SaludorEn{}, nil
}

Y ahora vamos a hacer un método que sea polimorfico que imprima el saludo : 


func Saludar(saludor Saludor) {
fmt.Println(saludor.GetSaludo())
}

Como ven referencia a la interfaz por lo tanto se puede usar con el SaludadorEs y el SaludadorEn porque implementan esta interfaz. 

Y por ultimo vamos hacer 2 app de fx una en español y la otra en ingles : 


func main() {
appEs := fx.New(

fx.Provide(
CrearSaludadorEs,
),

fx.Invoke(Saludar),
)

ctxEs, cancelEs := context.WithCancel(context.Background())
defer cancelEs()
if err := appEs.Start(ctxEs); err != nil {
log.Fatal(err)
}

appEn := fx.New(

fx.Provide(
CrearSaludadorEn,
),

fx.Invoke(Saludar),
)

ctxEn, cancelEn := context.WithCancel(context.Background())
defer cancelEn()
if err := appEn.Start(ctxEn); err != nil {
log.Fatal(err)
}
}

y la salida es la siguiente : 

[Fx] PROVIDE    main.Saludor <= main.CrearSaludadorEs()
[Fx] PROVIDE    fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE    fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE    fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] INVOKE             main.Saludar()
Hola !!
[Fx] RUNNING
[Fx] PROVIDE    main.Saludor <= main.CrearSaludadorEn()
[Fx] PROVIDE    fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE    fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE    fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] INVOKE             main.Saludar()
Hi !!
[Fx] RUNNING

Y listo!!



Un Ejemplo de polimorfismo en golang

 


Si seguís el blog tenes que saber más o menos que es el polimorfismo. En golang no tenemos clases pero tenemos interfaces, por lo tanto podemos utilizar el polimorfismo. 


Supongamos que tenemos que imprimir diferentes cosas documentos de texto, planillas, etc. Y queremos que una impresora pueda imprimir cualquier documento. Por lo tanto la impresora va a tomar un objeto imprimible y lo imprimira. 

Entonces vamos a hacer la interfaz imprimible. 


type Imprimible interface {

to_string() string

}


Y ahora vamos a hacer estructuras que implementen la interfaz : 


type Doc struct {

texto string

}


type Datos struct {

dato1 string

dato2 string

}


func (doc Doc) to_string() string {

return doc.texto

}


func (datos Datos) to_string() string {

return datos.dato1 + " " + datos.dato2

}


Y ahora vamos a hacer la función imprimir que para que funcione el ejemplo va a imprimir por pantalla: 


func imprimir(imprimible Imprimible) {

fmt.Println(imprimible.to_string())

}


Y por ultimo vamos a utilizar esta función: 



func main() {

var doc = Doc{texto: "Hola!! Este es Doc"}

var datos = Datos{dato1: "dato1", dato2: "dato2"}

imprimir(doc)

imprimir(datos)

}


Y listo!!! 

Inyección de dependencia en Go usando Fx


Vamos por parte, que es la inyección de dependencias?

Cualquier aplicación no trivial está formada por dos o más objetos que colaboran entre sí para realizar alguna lógica. Tradicionalmente cada objeto se hacía cargo de obtener sus propias referencias a los objetos a los cuales colaboraba (sus dependencias). Esto lleva a código acoplado y difícil de probar. Y para solucionar esto esta la inyección de dependencias. El contexto ingresa las dependencias de un objeto. Para mayor información pueden ir a : https://emanuelpeg.blogspot.com/2009/07/inyeccion-de-dependencias.html


Y que es Fx? Según la documentación oficial publicada por Uber, Fx es un framework para Go que:

  • Facilita la inyección de dependencia.
  • Elimina la necesidad de estado global y func init().

Fx usa el patrón de inyección de constructor, tratemos de entender exactamente cómo hace que la inyección de dependencia sea fácil en Go.

Para usar Fx debemos: 

El primer paso es instalar la biblioteca a través de go get go.uber.org/fx.

Y luego podemos hacer esto: 

package main


import (

"context"

"go.uber.org/fx"

"go.uber.org/fx/fxevent"

"log"

"net"

"net/http"

"os"

"time"

)


func NewLogger() *log.Logger {

logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */)

logger.Print("Create NewLogger.")

return logger

}


func NewHandler(logger *log.Logger) (http.Handler, error) {

logger.Print("Create NewHandler.")

return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {

logger.Print("Got a request.")

}), nil

}


func NewMux(lc fx.Lifecycle, logger *log.Logger) *http.ServeMux {

logger.Print("Create NewMux.")

mux := http.NewServeMux()

server := &http.Server{

Addr:    "127.0.0.1:8080",

Handler: mux,

}

lc.Append(fx.Hook{

OnStart: func(context.Context) error {

logger.Print("Starting HTTP server.")

ln, err := net.Listen("tcp", server.Addr)

if err != nil {

return err

}

go server.Serve(ln)

return nil

},

OnStop: func(ctx context.Context) error {

logger.Print("Stopping HTTP server.")

return server.Shutdown(ctx)

},

})


return mux

}


func Register(mux *http.ServeMux, h http.Handler) {

mux.Handle("/", h)

}


func main() {

app := fx.New(


fx.Provide(

NewLogger,

NewHandler,

NewMux,

),


fx.Invoke(Register),


fx.WithLogger(

func() fxevent.Logger {

return fxevent.NopLogger

},

),

)


startCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)

defer cancel()

if err := app.Start(startCtx); err != nil {

log.Fatal(err)

}


if _, err := http.Get("http://localhost:8080/"); err != nil {

log.Fatal(err)

}


stopCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)

defer cancel()

if err := app.Stop(stopCtx); err != nil {

log.Fatal(err)

}


}


En el ejemplo queremos costruir varias entidades, un *log.Logger,  un http.Handler, un servidor *http.ServeMux y queremos llamar a una request. Y fx, nos construye estos objetos. 

Si ejecutamos esta aplicación el resultado será : 

Create NewLogger.
Create NewMux.
Create NewHandler.
Starting HTTP server.
Got a request.
Stopping HTTP server.


Dejo link: 


viernes, 24 de febrero de 2023

Programación poliglota con GraalVM parte 2


Seguimos con GraalVM 

Veamos un ejemplo queremos ejecutar una aplicación javascript o node que dentro llama a codigo ruby, por ejemplo. 

Vamos a tener que crear un archivo .js que lo llamaré ejemplo.js que contenga lo siguiente: 


var array = Polyglot.eval("ruby", "[1,2,42,4]")

console.log(array[2]);


Y lo ejecutamos con : 


js --polyglot --jvm ejemplo.js

42

node --polyglot --jvm ejemplo.js

42


Así de fácil o queremos ejecutar en una aplicación R, javascript :

array <- eval.polyglot("js", "[1,2,42,4]")
print(array[3L])

Y lo corremos con : 

Rscript --polyglot --jvm ejemplo.R
[1] 42

Y puedo seguir con ejemplos pero creo que se entiende :D


Dejo link : https://www.graalvm.org/reference-manual/polyglot-programming/#running-polyglot-applications

martes, 21 de febrero de 2023

Probar código asíncrono con cats

Cómo simplificar las pruebas unitarias para código asincrónico haciéndolas sincrónicas con Cats? Esta es la pregunta que vamos a intantar responder. 

Supongamos que estamos midiendo el tiempo de actividad en un conjunto de servidores.  Habrá dos componentes. El primero es un UptimeClient que sondea servidores remotos para conocer su tiempo de actividad:


import scala.concurrent.Future

trait UptimeClient {

    def getUptime(hostname: String): Future[Int]

}


También tendremos un UptimeService que mantiene una lista de servidores y permite al usuario sondearlos por su tiempo de actividad total:


import cats.instances.future._ // for Applicative

import cats.instances.list._// for Traverse

import cats.syntax.traverse._// for traverse

import scala.concurrent.ExecutionContext.Implicits.global


class UptimeService(client: UptimeClient) {

    def getTotalUptime(hostnames: List[String]): Future[Int] =

        hostnames.traverse(client.getUptime).map(_.sum)

}

Hemos modelado UptimeClient como un trait porque vamos a querer probarlo en pruebas unitarias. Por ejemplo, podemos escribir un cliente de prueba que nos permita proporcionar datos ficticios en lugar de llamar a servidores reales:


class TestUptimeClient(hosts: Map[String, Int]) extends UptimeClient {

    def getUptime(hostname: String): Future[Int] =

        Future.successful(hosts.getOrElse(hostname, 0))

}


Ahora, supongamos que estamos escribiendo pruebas unitarias para UptimeService. Queremos probar su capacidad para sumar valores, independientemente de dónde los obtenga. Aquí hay un ejemplo:


def testTotalUptime() = {

   val hosts= Map("host1" -> 10, "host2" -> 6)

   val client= new TestUptimeClient(hosts)

   val service= new UptimeService(client)

   val actual= service.getTotalUptime(hosts.keys.toList)

   val expected = hosts.values.sum

   assert(actual == expected)

}


El código no compila porque hemos cometido un error clásico. Olvidamos que nuestro código de aplicación es asíncrono. Nuestro resultado real es de tipo Future[Int] y nuestro resultado esperado es de tipo Int. ¡No podemos compararlos directamente!

Hay un par de maneras de resolver este problema. Podríamos modificar nuestro código de prueba para acomodar la asincronía. Sin embargo, existe otra alternativa. ¡Hagamos que nuestro código de servicio sea sincrónico para que nuestra prueba funcione sin modificaciones!

Necesitamos implementar dos versiones de UptimeClient: una asíncrona para usar en producción y una síncrona para usar en nuestras pruebas unitarias:


trait RealUptimeClient extends UptimeClient {

    def getUptime(hostname: String): Future[Int]

}

trait TestUptimeClient extends UptimeClient {

    def getUptime(hostname: String): Int

}


La pregunta es: ¿qué tipo de resultado debemos dar al método abstracto en UptimeClient? Future[Int] o Int:


trait UptimeClient {

    def getUptime(hostname: String): ???

}


Al principio esto puede parecer difícil. Pero afortunadamente, Cats brinda una solución en términos del tipo de identidad, Id. Id nos permite "envolver" tipos en un constructor de tipos sin cambiar su significado:


package cats

type Id[A] = A


Id nos permite abstraernos sobre los tipos de devolución en UptimeClient. Implementa esto ahora:


import cats.Id

trait UptimeClient[F[_]] {

    def getUptime(hostname: String): F[Int]

}

trait RealUptimeClient extends UptimeClient[Future] {

   def getUptime(hostname: String): Future[Int]

}

trait TestUptimeClient extends UptimeClient[Id] {

   def getUptime(hostname: String): Id[Int]

}


Ahora deberíamos poder desarrollar una definición de TestUptimeClient en una clase completa basada en Map[String, Int].


Dirijamos nuestra atención a UptimeService. Necesitamos reescribirlo para abstraer los dos tipos de UptimeClient:

class UptimeService[F[_]](client: UptimeClient[F]) {

    def getTotalUptime(hostnames: List[String]): F[Int] =

        hostnames.traverse(client.getUptime).map(_.sum)

}

Ahora descomente el cuerpo de getTotalUptime. Debería obtener un error de compilación similar al siguiente:


// <console>:28: error: could not find implicit value for

//evidence parameter of type cats.Applicative[F]

//hostnames.traverse(client.getUptime).map(_.sum)

//                              ^


El problema aquí es que traverse solo funciona en secuencias de valores que tienen un Aplicativo. En nuestro código original estábamos atravesando una Lista[Futuro[Int]]. Hay un aplicativo para Future, así que estuvo bien.

En esta versión estamos atravesando una Lista[F[Int]]. Necesitamos demostrarle al compilador que F tiene un Aplicativo. Haga esto agregando un parámetro de constructor implícito a UptimeService.


import cats.Applicative

import cats.syntax.functor._ // for map


class UptimeService[F[_]](client: UptimeClient[F]) (implicit a: Applicative[F]) {

    def getTotalUptime(hostnames: List[String]): F[Int] =

        hostnames.traverse(client.getUptime).map(_.sum)

}


Finalmente, dirijamos nuestra atención a nuestras pruebas unitarias. Nuestro código de prueba ahora funciona según lo previsto sin ninguna modificación. Creamos una instancia de TestUptimeClient y la envolvemos en un UptimeService. Esto une efectivamente F a Id, lo que permite que el resto del código funcione sincrónicamente sin preocuparse por las mónadas o los aplicativos:


def testTotalUptime() = {

    val hosts= Map("host1" -> 10, "host2" -> 6)

    val client= new TestUptimeClient(hosts)

    val service= new UptimeService(client)

    val actual= service.getTotalUptime(hosts.keys.toList)

    val expected = hosts.values.sum

    assert(actual == expected)

}

testTotalUptime()


Este estudio de caso proporciona un ejemplo de cómo Cats puede ayudarnos a abstraernos en diferentes escenarios computacionales. Usamos la clase de tipo Applicative para abstraer sobre código asíncrono y síncrono. Apoyarnos en una abstracción funcional nos permite especificar la secuencia de cálculos que queremos realizar sin preocuparnos por los detalles de la implementación.

Las clases de tipo como Functor, Applicative, Monad y Traverse proporcionan implementaciones abstractas de patrones como mapeo, compresión, secuenciación e iteración. Las leyes matemáticas de esos tipos aseguran que funcionen junto con un conjunto consistente de semántica.

Usamos Applicative en este caso de estudio porque era la clase de tipos menos poderosa que hacía lo que necesitábamos. Si hubiéramos requerido flatMap, podríamos haber cambiado Applicative por Monad. Si hubiéramos necesitado abstraernos sobre diferentes tipos de secuencias, podríamos haber usado Traverse. También hay clases de tipos como ApplicativeError y MonadError que ayudan a modelar fallas y cálculos exitosos.

lunes, 20 de febrero de 2023

Tutoriales de kubernetes


Este post es una pavada, pero bueno quiero compartirlo igual. Encontré un conjunto de tutoriales de kubernete en español en la pagina de kubernetes. No es una super novedad pero me sirvieron mucho. 

Dejo link : https://kubernetes.io/es/docs/tutorials/

jueves, 16 de febrero de 2023

¿Cuál es la diferencia entre "is not null" y "!= null" en C# ?



La principal diferencia entre e != null y is not null es la forma en que el compilador ejecuta la comparación.

El compilador garantiza que no se invoque ningún operador de igualdad == sobrecargado por el usuario cuando se evalúa la expresión x es nula.

Es decir si sobre escriben mal == estamos muertos, por eso esta bueno siempre usar "is not null" o "is null" 

Veamos un ejemplo: 

public class TestObject

{

  public string Test { get; set; }


  // attempt to allow TestObject to be testable against a string

  public static bool operator ==(TestObject a, object b)

  {

    if(b == null)

      return false;

    

    if(b is string)

      return a.Test == (string)b;


    if(b is TestObject)

      return a.Test == ((TestObject)b).Test;


    return false;

  }


  public static bool operator !=(TestObject a, object b)

  {

    if(b == null)

      return false;

    

    if(b is string)

      return a.Test != (string)b;


    if(b is TestObject)

      return a.Test != ((TestObject)b).Test;


    return false;

  }

}


Como vemos la sobrecarga de == y != cuando b es nulo, retorna false y eso esta mal. Por lo tanto, si ejecutamos el siguiente código:


TestObject e = null;

if(e == null)

  Console.WriteLine("e == null");

if(e is null)

  Console.WriteLine("e is null");


El Output va ser : e is null

Y si hacemos : 


TestObject e = new TestObject();

if(e != null)

  Console.WriteLine("e != null");

if(e is not null)

  Console.WriteLine("e is not null");


El Output va ser: e is not null

Ninguno de los operadores sobrecargados se implementa "correctamente", por lo que la consola nunca genera e == null o e != null.