Scala nauka - interpreter Brainfucka

0

W ramach nauki Scali zrobiłem zadanie z https://www.hackerrank.com/challenges/brainf-k-interpreter-fp/problem, polegające na stworzeniu interpretera Brainfucka.

Popełniłem kawałek kodu, co do którego mam wątpliwości:

  1. Czy używanie return/ifów jest "scalowe" ?
  2. Czy rzucają się w oczy inne rzeczy, które nie pasują do podejścia funkcyjnego?
import scala.annotation.tailrec

object Solution {
    
  def executeBrainFuck(input: List[Byte], code: Array[Char]) = {
      
    def incDV(k : Int) = if (k==255) 0 else k+1
    def decDV(k : Int) = if (k==0) 255 else k-1
      
    @tailrec
    def _executeBF(
                     opCounter: Int, // operations counter
                     data: Map[Int, Int], // data segment
                     input: List[Byte], // input segment
                     code: Array[Char], // code segment
                     codePointer: Int,  // pointer to the next instruction
                     dataPointer: Int,  // pointer to data cell
                     brackets: Map[Int, Int] // opening/closing brackets information 
                   ) :Unit = {
      if (opCounter == 100000 ) {
        println("\nPROCESS TIME OUT. KILLED!!!")
        return None
      }

      var newDataPointer = dataPointer
      var newCodePointer = codePointer + 1
      var newInput = input
      var newData = data

      // update data/input/pointers as per the operation semantics
      code(codePointer) match {
        case '>' => newDataPointer += 1
        case '<' => newDataPointer -= 1
        case '+' => newData = data.updated(dataPointer,incDV(data.getOrElse(dataPointer, 0)))
        case '-' => newData = data.updated(dataPointer,decDV(data.getOrElse(dataPointer, 0)))
        case '.' => print(data.getOrElse(dataPointer,0).toChar)
        case '[' => if (data.getOrElse(dataPointer, 0) == 0) newCodePointer = brackets(codePointer)
        case ']' => if (data.getOrElse(dataPointer, 0) != 0) newCodePointer = brackets(codePointer)
        case ',' => if (!newInput.isEmpty) {
          newData = data.updated(dataPointer,newInput.head)
          newInput = newInput.tail
        }
      } // opcode

      if (newCodePointer<code.length)
        _executeBF(opCounter + 1, newData, newInput, code, newCodePointer, newDataPointer, brackets)
    }

    // For given BF code returns list of pairs:
    // (index of a bracket, index of the corresponding bracket (either closing or opening) )
    def pairBrackets(code: List[Char]): List[(Int, Int)] = {
      // 1. assign index for each character
      // 2. filter everything but square brackets
      // 3. compute pairs
      code
        .zipWithIndex
        .filter(x => x._1 == ']' || x._1 == '[')
        .foldLeft((List[(Int, Int)](), List[(Char, Int)]())) {
          // acc(1) - accumulator for holding pairs
          // acc(2) - stack used for construction
          case (acc, e) =>

            // e is ( _1 =bracket, _2 = bracket's index)
            e._1 match {
              // push opening bracket to the stack
              case '[' => (acc._1, List(e) ++ acc._2)

              // pop a bracket from the stack and accumulate: A:B, as A-B and B-A
              case _ => (List[(Int, Int)]((acc._2.head._2, e._2),(e._2,acc._2.head._2)) ++ acc._1, acc._2.tail)
            }
        }._1 // return list of pairs
    }

    // prepare brackets information
    val brackets = pairBrackets(code.toList).toMap[Int,Int]

    // execute the code
    _executeBF(0, Map[Int,Int](), input, code, 0, 0, brackets)
  }


    def main(args: Array[String]) {

      // parse HackerRank input 
      val lines = io.Source.stdin.getLines.toList
      
      val nm = lines.take(1)(0).split(" ")
      val n = nm(0).toInt
      val m = nm(1).toInt
        
      // input data for BF interpreter
      val input = lines.drop(1).head.toList.filter(_ != '$').map(_.toByte)
        
      // code to be executed, skip all the garbage
      val code = lines.drop(2).take(m).map(_.toCharArray).flatten.filter("]><+-.,[".toCharArray.contains(_)).toArray
      
      // call the interpreter
      executeBrainFuck(input,code)
    }
}
1

Returnów nie należy używać. Więcej można poczytać tutaj: https://www.scala-lang.org/files/archive/spec/2.11/06-expressions.html#return-expressions

If'y są ok pod warunkiem, że używane są jako coś, co zwraca konkretną wartość.

Pobieżnie patrząc na kod:

*Dużo var'ów które w większości przypadków nie są potrzebne. Powinno się stosować val.
*W match nie sprawdzasz wszystkich możliwości. Powinien Ci to zgłosić kompilator.
*Zwracanie Unit'ów jest raczej złą praktyką
*Kod w stylu ```
.filter(x => x._1 == ']' || x._1 == '[')

1

Dopełniając odpowiedź @Krwawy Lew (byłem niezalogowany):
Programowania funkcyjnego tutaj za dużo nie ma. Var'y tylko w specyficznych przypadkach spełniają założenia programowania funkcyjnego (np. jako mutowalna kolekcja, która zwiększa wydajność danej funkcji, jednakże jest to tylko jej szczegół implementacyjny przez co z zewnątrz nadal pozostaje "czysta"). Nie wczytywałem się czy tutaj tak jest, ale fakt, że trzeba spędzić nad kodem dłuższą chwilę żeby to wywnioskować powoduje, że jest już z nim coś nie tak.

Tak jak wyżej napisałem, wszystko co zwraca "Unit" jest złe, a tym bardziej jeśli mówimy w kontekście programowania funkcyjnego. Oczywiście nie zawsze da się to spełnić (albo nie zawsze ma sens przestrzeganie tego), jednakże powinno się to zdarzać w miarę rzadko.
Twoja główna funkcja _executeBF jest:
*Źle nazwana, nie używa się podłóg w nazwach w Scali
*Zwraca Unit
*Przyjmuje zbyt dużo argumentów. Stwórz jakieś struktury danych, korzystaj z systemu typów który oferuje Scala itd. Jeśli musisz objaśniać co robi dany argument to znaczy, że jest coś nie tak z daną funkcją/metodą
*Ostatnią wartością zwracaną w tej funkcji jest if bez else, jest to złe, więcej do poczytania tutaj: https://stackoverflow.com/questions/45619279/what-is-the-return-type-of-a-scala-if-statement-without-else

Poza tym metody są dość rozbudowane, przydałoby się je podzielić na mniejsze. To, że można pisać funkcyjnie nie powoduje, że należy pomijać aspekt programowania obiektowego. Można ten program podzielić na kilka klas, abstrakcji itd. Większość zmiennych powinna mieć inne nazwy, te które są obecnie nic nie mówią. Jeśli korzystasz z syntax'u ._1 itd. to upewnij się, że wiadomo czym jest owe ._1. Nie korzystasz z takich typów jak Try, Option, Either, Future. Nawet jeśli większość z nich nie miałaby tutaj sensu to dla ćwiczeń warto sobie sprawdzić jak działają, czym się charakteryzują, co sprawia że są monadami (lub prawie są) itd.

0

@DisQ: dzięki za uwagi, przemyślę je sobie, doczytam i przerobię kod. Wszystkiego na raz w jednym ćwiczeniu na pewno nie uda mi się zastosować, dlatego robię małe kroki.

W tym ćwiczeniu przerobię funkcję tak by dostawała stan i instrukcję do wykonania, po zaaplikowaniu instrukcji na stanie zwróci stan. W ten sposób wykonanie programu będzie serią instrukcji zaaplikowanych na stanie (czyli zgrubnie to co teraz, z tym, że stan przekazywany jest via "wiele parametrów"). Zobaczę co z tego wyjdzie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1