Skip to content

Scala to C# Conversion Guide

Alex Valuyskiy edited this page Jun 18, 2017 · 14 revisions
  • Be .NET idiomatic, e.g. do not port Duration instead of TimeSpan and Future instead of Task<T>
  • Stay as close as possible to the original JVM implementation,
  • Do not add features that don't exist in JVM Akka into the core Akka.NET

Case classes

from

final case class HandingOverData(singleton: ActorRef, name: String)

All parameters of the case class should become a public property

simple implementation

public sealed class HandingOverData
{
    public HandingOverData(IActorRef singleton, string name)
    {
        Singleton = singleton;
        Name = name;
    }

    public IActorRef Singleton { get; }

    public string Name { get; }
}

complex implementation

public sealed class HandingOverData
{
    public HandingOverData(IActorRef singleton, string name)
    {
        Singleton = singleton;
        Name = name;
    }

    public IActorRef Singleton { get; }

    public string Name { get; }

    private bool Equals(HandingOverData other)
    {
        return Equals(Singleton, other.Singleton) && string.Equals(Name, other.Name);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        return obj is HandingOverData && Equals((HandingOverData)obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return ((Singleton?.GetHashCode() ?? 0) * 397) ^ (Name?.GetHashCode() ?? 0);
        }
    }

    public override string ToString() => $"{nameof(HandingOverData)}<{nameof(Singleton)}: {Singleton}, {nameof(Name)}: {Name}>";
}

In order to support C# 7 deconstruction you could add Deconstruct method

public void Deconstruct(out IActorRef singleton, out string name)
{
    singleton = Singleton;
    name = Name;
}

Some messages should implement With (from C# 8 records spec) or Copy method.

public HandingOverData With(IActorRef singleton = null, string name = null) 
    => new HandingOverData(singleton ?? Singleton, name ?? Name);

In some cases it would be a good idea (mandatory for value types) to implement IEquatable<T>

public sealed class HandingOverData : IEquatable<HandingOverData>
{
    ...
    public bool Equals(HandingOverData other)
    {
        return Equals(Singleton, other.Singleton) && string.Equals(Name, other.Name);
    }
    ...
}

case object

from

case object RecoveryCompleted

simple implementation

public sealed class RecoveryCompleted
{
    public static RecoveryCompleted Instance { get; } = new RecoveryCompleted();
    private RecoveryCompleted() {}
}

complex implementation

public sealed class RecoveryCompleted
{
    public static RecoveryCompleted Instance { get; } = new RecoveryCompleted();

    private RecoveryCompleted() {}
    public override bool Equals(object obj) => !ReferenceEquals(obj, null) && obj is RecoveryCompleted;
    public override int GetHashCode() => nameof(RecoveryCompleted).GetHashCode();
    public override string ToString() => nameof(RecoveryCompleted);
}

In some cases it would be a good idea (mandatory for value types) to implement IEquatable<T>

public sealed class RecoveryCompleted : IEquatable<RecoveryCompleted>
{
    ...
    public bool Equals(RecoveryCompleted other) => true;
    ...
}

Class constructors

from

class Person(name: String, var surname: string, val age: Int, private val position: String)
{
}

to

public class Person
{
    private readonly string _name;
    private readonly string _position;
    
    public Person(string name, string surname, int age, string position)
    {
        _name = name;
        Surname = surname;
        Age = age;
        _position = position;
    }
    
    public string Surname { get; set; }
    public int Age { get; }
}

LINQ (collection's methods)

scala C#
collectFirst(func) FirstOrDefault(func)
drop(count) Skip(count)
dropRight(count) Reverse().Skip(count).Reverse()
exists(func) Any(func)
filter(func) Where(func)
filterNot(func) <none>
flatMap(func) SelectMany(func)
flatten SelectMany(i => i)
fold(initval)(func) <none>
foldLeft(initval)(func) Aggregate(initval, func)
foldRight(initval)(func) Reverse().Aggregate(initval, func)
forall(func) All(func)
foreach(func) <none>
head First()
headOption FirstOrDefault()
last Last()
lastOption LastOrDefault()
map(func) Select(func)
mkString(separator) string.Join(separator, array)
reduce(func) <none>
reduceLeft(func) Aggregate(func)
reduceRight(func) Reverse().Aggregate(func)
sorted(implicit ordering) OrderBy(i -> i, comparer)
tail Skip(1)
take(count) Take(count)
takeRight(count) Reverse().Take(count).Reverse()
zip(seq2, func) Zip(seq2, func)
zipWithIndex <none>

Currying

from

def bufferOr(grouping: String, message: Any, originalSender: ActorRef)(action:  Unit): Unit = {
    buffers.get(grouping) match {
      case None  action
      case Some(messages) 
        buffers = buffers.updated(grouping, messages :+ ((message, originalSender)))
        totalBufferSize += 1
    }
}

to

public void BufferOr(string grouping, object message, IActorRef originalSender, Action action)
{
    BufferedMessages messages = null;
    if (_buffers.TryGetValue(grouping, out messages))
    {
        _buffers[grouping].Add(new KeyValuePair<object, IActorRef>(message, originalSender));
        _totalBufferSize += 1;
    }
    else {
        action();
    }  
}

Exceptions

scala C#
IllegalArgumentException ArgumentException
IllegalStateException InvalidOperationException
ArithmeticException ArithmeticException
NullPointerException NullReferenceException
NotSerializableException SerializationException

Pattern matching

constant patterns

def testPattern(x: Any): String = x match {
   case 0 => "zero"
   case true => "true"
   case "hello" => "you said 'hello'"
   case Nil => "an empty List"
}

C# 7 supports all constant patterns

public string TestPattern(object x)
{
    switch(x)
    {
        case 0: return "zero";
        case true: return "true";
        case "hello": return "you said Hello";
        case null: return "an empty list";                
    }
    return string.Empty;
}

sequence patterns

def testPattern(x: Any): String = x match {
   case List(0, _, _) => "a three-element list with 0 as the first element"
   case List(1, _*) => "a list beginning with 1, having any number of elements"
   case Vector(1, _*) => "a vector starting with 1, having any number of elements"
}

C# does not support sequence patterns

tuples patterns

def testPattern(x: Any): String = x match {
   case (a, b) => s"got $a and $b"
   case (a, b, c) => s"got $a, $b, and $c"
}

C# does not support tuples patterns

constructor patterns (case classes)

def testPattern(x: Any): String = x match {
   case Person(first, "Alexander") => s"found an Alexander, first name = $first"
   case Dog("Suka") => "found a dog named Suka"
}

C# does not support constructor patterns. But you could use an equivalent

public string TestPattern(object x)
{
    switch(x)
    {
        case Person p when p.LastName == "Alexander": return $"found an Alexander, first name = {p.FirstName}";
        case Dog d when d.Name == "Suka": return "found a dog named Suka";  
    }
    return string.Empty;
}

typed patterns

def testPattern(x: Any): String = x match {
   case s: String => s"you gave me this string: $s"
   case i: Int => s"thanks for the int: $i"
   case f: Float => s"thanks for the float: $f"
   case a: Array[Int] => s"an array of int: ${a.mkString(",")}"
   case as: Array[String] => s"an array of strings: ${as.mkString(",")}"
   case d: Dog => s"dog: ${d.name}"
   case list: List[_] => s"thanks for the List: $list"
   case m: Map[_, _] => m.toString
}

You can use typed patterns in C#7

public string TestPattern(object x)
{
    switch(x)
    { 
        case string s: return $"you gave me this string: {s}";
        case int i: return $"thanks for the int: {i}";
        case float f: return $"thanks for the float: {f}";
        case int[] a: return $"an array of int: {string.Join(",", a)}";
        case Dog d: return $"dog: ${d.Name}";
        case List<int> list: return $"thanks for the List: {list}";
        case Dictionary<int, string> dict: return $"dictionary: {dict}";
    }
    return string.Empty;
}

patterns on Option

from

def matchingRole(member: Member, role: String): Boolean = role match {
    case None     true
    case Some(r)  member.hasRole(r)
}

to

public bool MatchingRole(Member member, string role)
{
    switch (member)
    {
        case Member m:
	    return m.HasRole(role);
        default:
	    return true;	
    }
}

extractors

trait User {
  def name: String
}
class FreeUser(val name: String) extends User
class PremiumUser(val name: String) extends User

object FreeUser {
  def unapply(user: FreeUser): Option[String] = Some(user.name)
}
object PremiumUser {
  def unapply(user: PremiumUser): Option[String] = Some(user.name)
}

// using
val user: User = new PremiumUser("Daniel")
user match {
  case FreeUser(name) => "Hello " + name
  case PremiumUser(name) => "Welcome back, dear " + name
}
public interface IUser
{
    string Name { get; }
}

public class FreeUser : IUser
{
    public FreeUser(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

public class PremiumUser : IUser
{
    public PremiumUser(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

public static class UserExtensions
{
    public static bool TryExtractName(this IUser user, out string name)
    {
        name = user.Name;
        return !string.IsNullOrEmpty(user.Name);
    }
}

// using
IUser user = new PremiumUser("Alex");

switch (user)
{
    case PremiumUser p when p.TryExtractName(out var name):
        Console.WriteLine(name);
        break;
}

Require

require(cost > 0, "cost must be > 0")

use ArgumentException

if (cost <= 0) throw ArgumentException("cost must be > 0", nameof(cost));

Not covered topics

  • Traits
  • Partial functions
  • Local functions/Nested Methods
  • Tail call recursion
  • For Comprehensions
  • Default Parameter Values
  • Implicit parameters

Tests

  • Prefer to use FluentAssertions in tests, instead of Xunit assertions and AkkaSpecExtensions

intercept[T]

Akka uses intecept to check that an exception was thrown

intercept[IllegalArgumentException] {
  val serializer = new MiscMessageSerializer(system.asInstanceOf[ExtendedActorSystem])
  serializer.manifest("INVALID")
}

in C# you have 2 options

var serializer = new MiscMessageSerializer(Sys.AsInstanceOf<ExtendedActorSystem>());

// use FluentAssertions
Action comparison = () => serializer.Manifest("INVALID");
comparison.ShouldThrow<ArgumentException>();

// use Xunit2 asserts
Assert.Throws<ArgumentException>(() => serializer.Manifest("INVALID"));