วิธีการเขียนโปรแกรมเชิงหน้าที่สำหรับเกมที่ง่ายขึ้นโดยใช้ Scala และ LWJGL


11

I ซึ่งเป็นโปรแกรมเมอร์ที่จำเป็นของ Java ต้องการที่จะเข้าใจวิธีการสร้าง Space Invaders แบบง่าย ๆ ตามหลักการออกแบบฟังก์ชั่น Programming (โดยเฉพาะ Referential Transparency) อย่างไรก็ตามในแต่ละครั้งที่ฉันลองนึกถึงการออกแบบฉันจะหลงทางในความแปรปรวนอย่างรุนแรงซึ่งเป็นความไม่แน่นอนที่เหมือนกันซึ่งเป็นที่รังเกียจของนักเขียนโปรแกรมการทำงาน

เป็นความพยายามที่จะเรียนรู้หน้าที่ Programming ฉันตัดสินใจที่จะพยายามที่จะสร้างเกมแบบโต้ตอบ 2D ง่ายมากช่องว่าง Invader (หมายเหตุขาดพหูพจน์) ในScalaใช้lwjgl นี่คือข้อกำหนดสำหรับเกมพื้นฐาน:

  1. ผู้ใช้จัดส่งที่ด้านล่างของหน้าจอเลื่อนไปทางซ้ายและขวาด้วยปุ่ม "A" และ "D" ตามลำดับ

  2. กระสุนของผู้ใช้ยิงพุ่งขึ้นมาเปิดใช้งานโดยตรงโดยสเปซบาร์โดยมีการหยุดชั่วคราวอย่างน้อยระหว่างนัดเป็น 0.5 วินาที

  3. กระสุนของเรือรบต่างด้าวยิงตรงลงโดยใช้เวลาสุ่ม 0.5 ถึง 1.5 วินาทีระหว่างการยิง

สิ่งที่จงใจทิ้งไว้จากเกมดั้งเดิมคือเอเลี่ยนของ WxH อุปสรรคในการป้องกันที่ย่อยสลายได้ x3 จานรองความเร็วสูงที่ด้านบนของหน้าจอ

ตกลงไปยังโดเมนปัญหาจริงแล้ว สำหรับฉันทุกส่วนที่กำหนดไว้ชัดเจน มันเป็นชิ้นส่วนที่ไม่สามารถกำหนดได้ซึ่งดูเหมือนจะปิดกั้นความสามารถของฉันในการพิจารณาวิธีการเข้าถึง ส่วนที่กำหนดขึ้นมานั้นเป็นวิถีกระสุนของกระสุนทันทีที่มีการเคลื่อนไหวอย่างต่อเนื่องของเอเลี่ยนและการระเบิดเนื่องจากการชนทั้งสอง (หรือทั้งสองอย่าง) ของเรือของผู้เล่นหรือมนุษย์ต่างดาว ชิ้นส่วนที่ไม่สามารถกำหนดค่าได้ (สำหรับฉัน) กำลังจัดการสตรีมอินพุตของผู้ใช้การจัดการดึงค่าสุ่มสำหรับการกำหนดกระสุนไฟเอเลี่ยนและการจัดการเอาต์พุต (ทั้งกราฟิกและเสียง)

ฉันสามารถทำ (และทำไปแล้ว) การพัฒนาเกมประเภทนี้ในช่วงหลายปีที่ผ่านมา อย่างไรก็ตามทั้งหมดนี้มาจากกระบวนทัศน์ที่จำเป็น และLWJGLยังจัดเตรียมSpace Invaders เวอร์ชัน Java ที่ง่ายมาก(ซึ่งฉันเริ่มย้ายไปยัง Scala โดยใช้ Scala เป็น Java-without-semicolons)

นี่คือลิงค์บางส่วนที่พูดถึงบริเวณนี้ซึ่งดูเหมือนจะไม่มีใครจัดการกับความคิดโดยตรงในลักษณะที่บุคคลที่มาจากการเขียนโปรแกรม Java / Imperative จะเข้าใจ:

  1. Retrogames ที่ใช้งานได้จริงตอนที่ 1 โดย James Hague

  2. โพสต์ Stack Overflow ที่คล้ายกัน

  3. เกม Clojure / Lisp

  4. เกม Haskell บนกองล้น

  5. Yampa (ใน Haskell) การเขียนโปรแกรมปฏิกิริยา

ดูเหมือนว่ามีความคิดบางอย่างในเกม Clojure / Lisp และ Haskell (พร้อมแหล่งที่มา) แต่น่าเสียดายที่ฉันไม่สามารถอ่าน / ตีความรหัสเป็นแบบจำลองทางจิตที่ทำให้รู้สึกถึงสมองที่จำเป็น Java ง่ายๆของฉัน

ฉันรู้สึกตื่นเต้นกับความเป็นไปได้ที่ FP มอบให้ฉันสามารถลิ้มรสความสามารถในการขยายขีดความสามารถแบบหลายเธรด ฉันรู้สึกว่าถ้าสามารถที่จะคร่ำครวญถึงความเรียบง่ายของเวลา + เหตุการณ์ + การสุ่มแบบจำลองสำหรับ Space Invader ที่สามารถดำเนินการได้โดยแยกส่วนที่กำหนดและไม่ขึ้นอยู่กับระบบที่ออกแบบมาอย่างเหมาะสมโดยไม่ทำให้มันรู้สึกเหมือนกับทฤษฎีคณิตศาสตร์ขั้นสูง ; เช่น Yampa ฉันจะตั้งค่า หากการเรียนรู้ระดับทฤษฎี Yampa ดูเหมือนว่าจำเป็นต้องสร้างเกมง่ายๆให้ประสบความสำเร็จดังนั้นค่าใช้จ่ายในการได้รับการฝึกอบรมและกรอบแนวคิดที่จำเป็นทั้งหมดจะทำให้ฉันเข้าใจประโยชน์ของ FP อย่างน้อย (สำหรับการทดลองเรียนรู้ที่ง่ายเกินไป )

ข้อเสนอแนะแบบจำลองที่เสนอใด ๆ วิธีการที่แนะนำในการเข้าใกล้โดเมนปัญหา


1
ฉันได้ลบบางส่วนเกี่ยวกับบล็อกของคุณออกจากคำถามเพราะไม่จำเป็นสำหรับคำถามนั้น อย่าลังเลที่จะใส่ลิงค์ไปยังบทความติดตามเมื่อคุณเข้ามาเขียน
yannis

@Yannis - เข้าใจแล้ว Tyvm!
chaotic3quilibrium

คุณขอให้สกาล่าซึ่งเป็นเหตุผลว่าทำไมนี่เป็นเพียงความคิดเห็น Caves of Clojureสามารถอ่านวิธีการใช้สไตล์โร๊คไลค์ FP ได้อย่างง่ายดาย มันจัดการกับรัฐโดยการส่งกลับภาพรวมของโลกที่ผู้เขียนสามารถทดสอบได้ มันค่อนข้างเท่ห์ บางทีคุณสามารถเรียกดูผ่านโพสต์และดูว่าส่วนใดของการใช้งานของเขาสามารถถ่ายโอนไปยัง Scala ได้อย่างง่ายดาย
IAE

คำตอบ:


5

การใช้สำนวน Scala / LWJGL ของ Space Invaders จะไม่เหมือนกับการใช้งาน Haskell / OpenGL การเขียนการใช้งาน Haskell อาจเป็นการออกกำลังกายที่ดีกว่าในความคิดของฉัน แต่ถ้าคุณต้องการที่จะยึดติดกับสกาล่าต่อไปนี้เป็นแนวคิดสำหรับวิธีการเขียนในสไตล์การใช้งาน

ลองใช้วัตถุที่ไม่เปลี่ยนรูปเท่านั้น คุณอาจมีGameวัตถุซึ่งถือPlayerเป็นSet[Invader](ให้แน่ใจว่าจะใช้immutable.Set) ฯลฯ ให้(มันยังอาจจะใช้เวลาฯลฯ ) และให้ชั้นเรียนอื่น ๆ วิธีการที่คล้ายกันPlayerupdate(state: Game): PlayerdepressedKeys: Set[Int]

สำหรับการสุ่มscala.util.Randomนั้นไม่เปลี่ยนรูปเหมือนของ Haskell System.Randomแต่คุณสามารถสร้างเครื่องกำเนิดไฟฟ้าที่ไม่น่าเชื่อถือของคุณเองได้ อันนี้ไม่มีประสิทธิภาพ แต่มันแสดงให้เห็นถึงความคิด

case class ImmutablePRNG(val seed: Long) extends Immutable {
    lazy val nextLong: (Long, ImmutableRNG) =
        (seed, ImmutablePRNG(new Random(seed).nextLong()))
    ...
}

สำหรับการป้อนข้อมูลด้วยแป้นพิมพ์ / เมาส์และการเรนเดอร์ไม่มีวิธีใดในการเรียกใช้ฟังก์ชันที่ไม่บริสุทธิ์ พวกมันไม่บริสุทธิ์ใน Haskell ด้วยเช่นกันพวกมันเพิ่งถูกห่อหุ้มไว้IOฯลฯ เพื่อให้ฟังก์ชันวัตถุที่แท้จริงของคุณนั้นบริสุทธิ์ทางเทคนิค (พวกเขาไม่ได้อ่านหรือเขียนสถานะของตัวเองพวกเขาอธิบายกิจวัตรที่ต้องทำและระบบรันไทม์ .

เพียงแค่ไม่ได้ใส่ I / O รหัสในวัตถุที่ไม่เปลี่ยนรูปของคุณเช่นGame, และPlayer Invaderคุณสามารถให้วิธี แต่ก็ควรมีลักษณะดังนี้Playerrender

render(state: Game, buffer: Image): Image

น่าเสียดายที่นี่ไม่เหมาะกับ LWJGL เนื่องจากเป็นแบบรัฐ แต่คุณสามารถสร้าง abstractions ของคุณเองได้ คุณสามารถมีImmutableCanvasคลาสที่เก็บ AWT Canvasและblit(และวิธีการอื่น ๆ ) สามารถโคลนต้นแบบCanvasส่งผ่านไปยังDisplay.setParentจากนั้นดำเนินการเรนเดอร์และส่งคืนใหม่Canvas(ใน wrapper ที่ไม่เปลี่ยนรูปของคุณ)


ปรับปรุง : นี่คือรหัส Java บางส่วนที่แสดงว่าฉันจะไปเกี่ยวกับเรื่องนี้อย่างไร (ฉันจะเขียนโค้ดเดียวกันเกือบทั้งหมดใน Scala ยกเว้นว่าเซตที่ไม่เปลี่ยนรูปนั้นมีอยู่แล้วในตัวและบางลูปสำหรับแต่ละอันจะถูกแทนที่ด้วยแผนที่หรือเท่า) ฉันสร้างผู้เล่นที่เคลื่อนที่ไปรอบ ๆ และยิงกระสุน แต่ฉัน ไม่ได้เพิ่มศัตรูเนื่องจากรหัสที่ได้รับมานานแล้ว ฉันทำทุกอย่างเกี่ยวกับการคัดลอก - เขียน - ฉันคิดว่านี่เป็นแนวคิดที่สำคัญที่สุด

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;

import static java.awt.event.KeyEvent.*;

// An immutable wrapper around a Set. Doesn't implement Set or Collection
// because that would require quite a bit of code.
class ImmutableSet<T> implements Iterable<T> {
  final Set<T> backingSet;

  // Construct an empty set.
  ImmutableSet() {
    backingSet = new HashSet<T>();
  }

  // Copy constructor.
  ImmutableSet(ImmutableSet<T> src) {
    backingSet = new HashSet<T>(src.backingSet);
  }

  // Return a new set with an element added.
  ImmutableSet<T> plus(T elem) {
    ImmutableSet<T> copy = new ImmutableSet<T>(this);
    copy.backingSet.add(elem);
    return copy;
  }

  // Return a new set with an element removed.
  ImmutableSet<T> minus(T elem) {
    ImmutableSet<T> copy = new ImmutableSet<T>(this);
    copy.backingSet.remove(elem);
    return copy;
  }

  boolean contains(T elem) {
    return backingSet.contains(elem);
  }

  @Override public Iterator<T> iterator() {
    return backingSet.iterator();
  }
}

// An immutable, copy-on-write wrapper around BufferedImage.
class ImmutableImage {
  final BufferedImage backingImage;

  // Construct a blank image.
  ImmutableImage(int w, int h) {
    backingImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
  }

  // Copy constructor.
  ImmutableImage(ImmutableImage src) {
    backingImage = new BufferedImage(
        src.backingImage.getColorModel(),
        src.backingImage.copyData(null),
        false, null);
  }

  // Clear the image.
  ImmutableImage clear(Color c) {
    ImmutableImage copy = new ImmutableImage(this);
    Graphics g = copy.backingImage.getGraphics();
    g.setColor(c);
    g.fillRect(0, 0, backingImage.getWidth(), backingImage.getHeight());
    return copy;
  }

  // Draw a filled circle.
  ImmutableImage fillCircle(int x, int y, int r, Color c) {
    ImmutableImage copy = new ImmutableImage(this);
    Graphics g = copy.backingImage.getGraphics();
    g.setColor(c);
    g.fillOval(x - r, y - r, r * 2, r * 2);
    return copy;
  }
}

// An immutable, copy-on-write object describing the player.
class Player {
  final int x, y;
  final int ticksUntilFire;

  Player(int x, int y, int ticksUntilFire) {
    this.x = x;
    this.y = y;
    this.ticksUntilFire = ticksUntilFire;
  }

  // Construct a player at the starting position, ready to fire.
  Player() {
    this(SpaceInvaders.W / 2, SpaceInvaders.H - 50, 0);
  }

  // Update the game state (repeatedly called for each game tick).
  GameState update(GameState currentState) {
    // Update the player's position based on which keys are down.
    int newX = x;
    if (currentState.keyboard.isDown(VK_LEFT) || currentState.keyboard.isDown(VK_A))
      newX -= 2;
    if (currentState.keyboard.isDown(VK_RIGHT) || currentState.keyboard.isDown(VK_D))
      newX += 2;

    // Update the time until the player can fire.
    int newTicksUntilFire = ticksUntilFire;
    if (newTicksUntilFire > 0)
      --newTicksUntilFire;

    // Replace the old player with an updated player.
    Player newPlayer = new Player(newX, y, newTicksUntilFire);
    return currentState.setPlayer(newPlayer);
  }

  // Update the game state in response to a key press.
  GameState keyPressed(GameState currentState, int key) {
    if (key == VK_SPACE && ticksUntilFire == 0) {
      // Fire a bullet.
      Bullet b = new Bullet(x, y);
      ImmutableSet<Bullet> newBullets = currentState.bullets.plus(b);
      currentState = currentState.setBullets(newBullets);

      // Make the player wait 25 ticks before firing again.
      currentState = currentState.setPlayer(new Player(x, y, 25));
    }
    return currentState;
  }

  ImmutableImage render(ImmutableImage img) {
    return img.fillCircle(x, y, 20, Color.RED);
  }
}

// An immutable, copy-on-write object describing a bullet.
class Bullet {
  final int x, y;
  static final int radius = 5;

  Bullet(int x, int y) {
    this.x = x;
    this.y = y;
  }

  // Update the game state (repeatedly called for each game tick).
  GameState update(GameState currentState) {
    ImmutableSet<Bullet> bullets = currentState.bullets;
    bullets = bullets.minus(this);
    if (y + radius >= 0)
      // Add a copy of the bullet which has moved up the screen slightly.
      bullets = bullets.plus(new Bullet(x, y - 5));
    return currentState.setBullets(bullets);
  }

  ImmutableImage render(ImmutableImage img) {
    return img.fillCircle(x, y, radius, Color.BLACK);
  }
}

// An immutable, copy-on-write snapshot of the keyboard state at some time.
class KeyboardState {
  final ImmutableSet<Integer> depressedKeys;

  KeyboardState(ImmutableSet<Integer> depressedKeys) {
    this.depressedKeys = depressedKeys;
  }

  KeyboardState() {
    this(new ImmutableSet<Integer>());
  }

  GameState keyPressed(GameState currentState, int key) {
    return currentState.setKeyboard(new KeyboardState(depressedKeys.plus(key)));
  }

  GameState keyReleased(GameState currentState, int key) {
    return currentState.setKeyboard(new KeyboardState(depressedKeys.minus(key)));
  }

  boolean isDown(int key) {
    return depressedKeys.contains(key);
  }
}

// An immutable, copy-on-write description of the entire game state.
class GameState {
  final Player player;
  final ImmutableSet<Bullet> bullets;
  final KeyboardState keyboard;

  GameState(Player player, ImmutableSet<Bullet> bullets, KeyboardState keyboard) {
    this.player = player;
    this.bullets = bullets;
    this.keyboard = keyboard;
  }

  GameState() {
    this(new Player(), new ImmutableSet<Bullet>(), new KeyboardState());
  }

  GameState setPlayer(Player newPlayer) {
    return new GameState(newPlayer, bullets, keyboard);
  }

  GameState setBullets(ImmutableSet<Bullet> newBullets) {
    return new GameState(player, newBullets, keyboard);
  }

  GameState setKeyboard(KeyboardState newKeyboard) {
    return new GameState(player, bullets, newKeyboard);
  }

  // Update the game state (repeatedly called for each game tick).
  GameState update() {
    GameState current = this;
    current = current.player.update(current);
    for (Bullet b : current.bullets)
      current = b.update(current);
    return current;
  }

  // Update the game state in response to a key press.
  GameState keyPressed(int key) {
    GameState current = this;
    current = keyboard.keyPressed(current, key);
    current = player.keyPressed(current, key);
    return current;
  }

  // Update the game state in response to a key release.
  GameState keyReleased(int key) {
    GameState current = this;
    current = keyboard.keyReleased(current, key);
    return current;
  }

  ImmutableImage render() {
    ImmutableImage img = new ImmutableImage(SpaceInvaders.W, SpaceInvaders.H);
    img = img.clear(Color.BLUE);
    img = player.render(img);
    for (Bullet b : bullets)
      img = b.render(img);
    return img;
  }
}

public class SpaceInvaders {
  static final int W = 640, H = 480;

  static GameState currentState = new GameState();

  public static void main(String[] _) {
    JFrame frame = new JFrame() {{
      setSize(W, H);
      setTitle("Space Invaders");
      setContentPane(new JPanel() {
        @Override public void paintComponent(Graphics g) {
          BufferedImage img = SpaceInvaders.currentState.render().backingImage;
          ((Graphics2D) g).drawRenderedImage(img, new AffineTransform());
        }
      });
      addKeyListener(new KeyAdapter() {
        @Override public void keyPressed(KeyEvent e) {
          currentState = currentState.keyPressed(e.getKeyCode());
        }
        @Override public void keyReleased(KeyEvent e) {
          currentState = currentState.keyReleased(e.getKeyCode());
        }
      });
      setLocationByPlatform(true);
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setVisible(true);
    }};

    for (;;) {
      currentState = currentState.update();
      frame.repaint();
      try {
        Thread.sleep(20);
      } catch (InterruptedException e) {}
    }
  }
}

2
ฉันเพิ่มรหัส Java - มันช่วยได้ไหม ถ้ารหัสดูแปลก ๆ ฉันจะดูตัวอย่างเล็ก ๆ ของคลาสที่ไม่เปลี่ยนรูปและคัดลอกเมื่อเขียน นี้ดูเหมือนจะเป็นคำอธิบายที่ดี
Daniel Lubarov

2
@ chaotic3quilibrium เป็นเพียงตัวระบุปกติ บางครั้งฉันใช้มันแทนที่จะเป็นargsถ้ารหัสไม่สนใจข้อโต้แย้ง ขออภัยในความสับสนที่ไม่จำเป็น
Daniel Lubarov

2
ไม่ต้องห่วง. ฉันแค่คาดเดาสิ่งนั้นและเดินหน้าต่อไป ฉันเล่นกับโค้ดตัวอย่างของคุณเมื่อวานนี้ ฉันคิดว่าฉันมีความคิด ตอนนี้ฉันสงสัยว่าฉันหายไปอย่างอื่น จำนวนวัตถุชั่วคราวมีความเป็นมนุษย์ เห็บทุกคู่สร้างเฟรมที่แสดง GameState และเพื่อไปที่ GameState จาก GameState ของเห็บก่อนหน้านั้นเกี่ยวข้องกับการสร้างอินสแตนซ์ของ GameState จำนวนหนึ่งโดยแต่ละครั้งมีการปรับเพียงเล็กน้อยจาก GameState ก่อนหน้า
chaotic3quilibrium

3
ใช่มันค่อนข้างสิ้นเปลือง ฉันไม่คิดว่าGameStateสำเนาจะมีราคาแพงถึงแม้ว่าหลาย ๆ สำเนาจะทำแต่ละเห็บ แต่การคัดลอกImmutableSets อาจมีราคาแพงหากกระสุนจำนวนมากยังมีชีวิตอยู่ในเวลาเดียวกัน เราสามารถแทนที่ImmutableSetด้วยโครงสร้างต้นไม้scala.collection.immutable.TreeSetเพื่อลดปัญหา
Daniel Lubarov

2
และImmutableImageยิ่งแย่กว่านั้นเพราะมันเป็นการคัดลอกแรสเตอร์ขนาดใหญ่เมื่อมันถูกดัดแปลง มีบางสิ่งที่เราสามารถทำได้เพื่อลดปัญหาดังกล่าวด้วย แต่ฉันคิดว่ามันจะเป็นประโยชน์มากที่สุดในการเขียนโค้ดการแสดงผลในรูปแบบที่จำเป็น
Daniel Lubarov

4

ทีนี้คุณกำลังลดความพยายามลงด้วยการใช้ LWJGL - ไม่ทำอะไรเลย แต่มันจะใช้สำนวนที่ไม่ใช้การได้

งานวิจัยของคุณสอดคล้องกับสิ่งที่ฉันแนะนำ "กิจกรรม" ได้รับการสนับสนุนอย่างดีในการเขียนโปรแกรมการทำงานผ่านแนวความคิดเช่นการเขียนโปรแกรมปฏิกิริยาการทำงานหรือการเขียนโปรแกรม dataflow คุณอาจลองใช้Reactiveซึ่งเป็นไลบรารี FRP สำหรับ Scala เพื่อดูว่ามีผลข้างเคียงหรือไม่

รวมทั้งนำหน้าออกจาก Haskell: ใช้ monads เพื่อแค็ปซูล / แยกผลข้างเคียง ดูสถานะและ Monads IO


Tyvm สำหรับการตอบกลับของคุณ ฉันไม่แน่ใจว่าจะรับแป้นพิมพ์ / เมาส์และเอาต์พุตกราฟิก / เสียงจากปฏิกิริยาได้อย่างไร ที่นั่นและฉันเพิ่งหายไปหรือไม่ สำหรับการอ้างอิงของคุณเกี่ยวกับการใช้ Monad ฉันเพิ่งจะเรียนรู้เกี่ยวกับพวกเขาและยังไม่เข้าใจว่า monad คืออะไร
chaotic3quilibrium

3

ส่วนที่ไม่ได้กำหนดไว้ (สำหรับฉัน) กำลังจัดการสตรีมอินพุตผู้ใช้ ... การจัดการเอาต์พุต (ทั้งกราฟิกและเสียง)

ใช่แล้ว IO ไม่ได้ถูก จำกัด และผลข้างเคียงของ "all about" นั่นไม่ใช่ปัญหาในภาษาที่ใช้งานไม่บริสุทธิ์เช่น Scala

การจัดการการดึงค่าสุ่มสำหรับการกำหนดกระสุนไฟต่างด้าว

คุณสามารถใช้เอาต์พุตของตัวสร้างตัวเลขเทียมเทียมเป็นลำดับอนันต์ ( Seqใน Scala)

...

โดยเฉพาะอย่างยิ่งคุณเห็นความจำเป็นในการเปลี่ยนแปลงหรือไม่ ถ้าฉันคาดหวังคุณอาจนึกถึงผีสางของคุณว่ามีตำแหน่งในอวกาศที่เปลี่ยนแปลงไปตามกาลเวลา คุณอาจพบว่ามีประโยชน์ที่จะคิดเกี่ยวกับ "zippers" ในบริบทดังกล่าว: http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php


ฉันไม่รู้ด้วยซ้ำว่าจะจัดโครงสร้างโค้ดเริ่มต้นอย่างไรเพื่อให้เป็นโปรแกรมเชิงฟังก์ชัน หลังจากนั้นฉันไม่เข้าใจเทคนิคที่ถูกต้อง (หรือที่ต้องการ) สำหรับการเพิ่มในรหัส "ไม่บริสุทธิ์" ฉันรู้ว่าฉันสามารถใช้ Scala เป็น "Java โดยไม่มีเครื่องหมายอัฒภาค" ฉันไม่ต้องการทำเช่นนั้น ฉันต้องการเรียนรู้วิธีที่ FP จัดการกับสภาพแวดล้อมแบบไดนามิกที่ง่ายมากโดยไม่ต้องพึ่งพาเวลาหรือการรั่วไหลของค่าที่ไม่แน่นอน นั่นทำให้รู้สึก?
chaotic3quilibrium
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.