All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kr.bydelta.koala.proc.processors.kt Maven / Gradle / Ivy

Go to download

KoalaNLP는 한국어 처리의 통합 인터페이스를 지향하는 Java/Kotlin/Scala Library의 묶음입니다.

There is a newer version: 2.1.4
Show newest version
@file:JvmName("ProcUtil")
package kr.bydelta.koala.proc

import kr.bydelta.koala.*
import kr.bydelta.koala.data.*
import java.util.*

/**
 * 문장분리기 Interface
 *
 * 텍스트를 받아서 텍스트 문장들로 분리합니다.
 *
 * ## 사용법 예제
 * 문장분리기 `SentenceSplitter`가 `CanSplitSentence`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * val splitter = SentenceSplitter()
 * val sentence = "분석할 문장을 적었습니다."
 * val split = splitter.sentences(sentence)
 * // 또는
 * val split = splitter(sentence)
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * val splitter = new SentenceSplitter()
 * val sentence = "분석할 문장을 적었습니다."
 * val split = splitter.sentences(sentence)
 * // 또는
 * val split = splitter(sentence)
 * ```
 *
 * ### Java
 * ```java
 * SentenceSplitter splitter = new SentenceSplitter();
 * String sentence = "분석할 문장을 적었습니다.";
 * List split = splitter.sentences(sentence);
 * // 또는
 * List split = splitter.invoke(sentence);
 * ```
 *
 * @since 1.x
 */
interface CanSplitSentence {
    /**
     * 주어진 문단 [text]를 문장단위로 분리합니다.
     *
     * @since 1.x
     * @param text 문장단위로 분리할 String.
     * @return 문장단위로 분리된 String의 [List].
     */
    fun sentences(text: String): List

    /**
     * 주어진 문단 [text]를 문장단위로 분리합니다.
     *
     * @since 2.0.0
     * @param text 문장단위로 분리할 String.
     * @return 문장단위로 분리된 String의 [List].
     */
    operator fun invoke(text: String): List = sentences(text)
}

/**
 * 세종 태그셋에 기반한 Heuristic 문장분리기
 *
 * 다음 조건에 따라 문장을 분리합니다:
 * 1. 열린 괄호나 인용부호가 없고,
 * 2. 숫자나 외국어로 둘러싸이지 않은 문장부호([POS.SF])가 어절의 마지막에 왔을 경우.
 *
 * ## 사용법 예제
 * `SentenceSplitter`는 이미 singleton object이므로 초기화가 필요하지 않습니다.
 *
 * ### Kotlin
 * ```kotlin
 * val sentence = ... //Tagged result
 * val split = SentenceSplitter.sentences(sentence)
 * // 또는
 * val split = SentenceSplitter(sentence)
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * val sentence = ... //Tagged result
 * val split = SentenceSplitter.sentences(sentence)
 * // 또는
 * val split = SentenceSplitter(sentence)
 * ```
 *
 * ### Java
 * ```java
 * Sentence sentence = ... //Tagged result
 * List split = SentenceSplitter.sentences(sentence);
 * // 또는
 * List split = SentenceSplitter.invoke(sentence);
 * ```
 *
 * @since 1.x
 */
object SentenceSplitter {
    private const val quoteRegex = "\'\"'""
    private const val openParenRegex = "([{‘“([{<〔〈《「『【"
    private const val closeParenRegex = ")]}’”)]}>〕〉》」』】"

    /**
     * 분석결과를 토대로 문장을 분리함.
     *
     * @since 2.0.0
     * @param para 분리할 문단.
     * @return 문장단위로 분리된 결과
     */
    @JvmStatic
    operator fun invoke(para: Iterable): List {
        val sentences = mutableListOf()
        val parenStack = Stack()
        val it = para.filter { it.isNotEmpty() }.toList()
        var begin = 0

        for (pos in 0 until it.size) {
            val word = it[pos]

            for (ch in word.surface) {
                if (ch in openParenRegex) {
                    parenStack.push(ch)
                } else if (ch in closeParenRegex) {
                    if (parenStack.isNotEmpty() &&
                            closeParenRegex.indexOf(ch) == openParenRegex.indexOf(parenStack.peek()))
                        parenStack.pop()
                } else if (ch in quoteRegex) {
                    if (parenStack.isNotEmpty() && parenStack.peek() == ch)
                        parenStack.pop()
                    else
                        parenStack.push(ch)
                }
            }

            if (word.last().tag == POS.SF) {
                val prevSLN =
                        if (word.size == 1) {
                            if (pos > 0) it[pos - 1].last().hasTagOneOf("SL", "SN") else false
                        } else {
                            word[word.size - 2].hasTagOneOf("SL", "SN")
                        }
                val nextSLN =
                        if (pos + 1 < it.size) {
                            it[pos + 1].first().hasTagOneOf("SL", "SN")
                        } else false

                if (!(prevSLN && nextSLN) && parenStack.isEmpty()) {
                    //This is the end of the sentence!
                    sentences.add(Sentence(it.subList(begin, pos + 1)))
                    begin = pos + 1
                }
            }
        }

        if (begin < it.size) {
            sentences.add(Sentence(it.subList(begin, it.size)))
        }

        return sentences.filter { it.isNotEmpty() }
    }

    /**
     * 분석결과를 토대로 문장을 분리함.
     *
     * @since 2.0.0
     * @param para 분리할 문단.
     * @return 문장단위로 분리된 결과
     */
    @JvmStatic
    fun sentences(para: Iterable) = invoke(para)
}

/**
 * 품사분석기의 최상위 인터페이스입니다.
 *
 * ## 참고
 * **형태소**는 의미를 가지는 요소로서는 더 이상 분석할 수 없는 가장 작은 말의 단위로 정의됩니다.
 *
 * **형태소 분석**은 문장을 형태소의 단위로 나누는 작업을 의미합니다.
 * 예) '문장을 형태소로 나눠봅시다'의 경우,
 * * 문장/일반명사, -을/조사,
 * * 형태소/일반명사, -로/조사,
 * * 나누-(다)/동사, -어-/어미, 보-(다)/동사, -ㅂ시다/어미
 * 로 대략 나눌 수 있습니다.
 *
 * 아래를 참고해보세요.
 * * [Morpheme] 형태소를 저장하는 클래스입니다.
 * * [POS] 형태소의 분류를 담은 Enum class
 *
 * ## 사용법 예제
 * 분석기 `Tagger`가 `CanTag`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * val tagger = Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * val tagger = new Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Java
 * ```java
 * Tagger tagger = new Tagger()
 * Sentence sentence = tagger.tagSentence("문장 1개입니다.")
 * List sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * List sentences = tagger.invoke("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * @since 1.x
 */
interface CanTag {
    /**
     * 주어진 문장 [text]을 분석하여 품사를 부착하고, 결과로 [Sentence] 객체를 돌려줍니다.
     *
     * @since 1.x
     * @param text 분석할 문장입니다.
     * @return 분석된 결과로 [Sentence] 객체 하나입니다.
     */
    fun tagSentence(text: String): Sentence

    /**
     * 주어진 문단 [text]을 분석하여 품사를 부착하고, 결과로 [List]<[Sentence]> 객체를 돌려줍니다.
     *
     * @since 1.x
     * @param text 분석할 문장입니다.
     * @return 분석된 결과로 [Sentence] 객체들의 목록입니다.
     */
    fun tag(text: String): List

    /**
     * 주어진 문단 [text]을 분석하여 품사를 부착하고, 결과로 [List]<[Sentence]> 객체를 돌려줍니다.
     *
     * @since 2.0.0
     * @param text 분석할 문장입니다.
     * @return 분석된 결과로 [Sentence] 객체들의 목록입니다.
     */
    operator fun invoke(text: String) = tag(text)
}

/**
 * 문장 1개가 분석가능한 품사분석기 interface. 원본 분석기는 문장 분석 결과를 [S] 타입으로 돌려줍니다.
 *
 * ## 참고
 * **형태소**는 의미를 가지는 요소로서는 더 이상 분석할 수 없는 가장 작은 말의 단위로 정의됩니다.
 *
 * **형태소 분석**은 문장을 형태소의 단위로 나누는 작업을 의미합니다.
 * 예) '문장을 형태소로 나눠봅시다'의 경우,
 * * 문장/일반명사, -을/조사,
 * * 형태소/일반명사, -로/조사,
 * * 나누-(다)/동사, -어-/어미, 보-(다)/동사, -ㅂ시다/어미
 * 로 대략 나눌 수 있습니다.
 *
 * 아래를 참고해보세요.
 * * [Morpheme] 형태소를 저장하는 클래스입니다.
 * * [POS] 형태소의 분류를 담은 Enum class
 *
 * ## 사용법 예제
 * 분석기 `Tagger`가 `CanTagASentence`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * val tagger = Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * val tagger = new Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Java
 * ```java
 * Tagger tagger = new Tagger()
 * Sentence sentence = tagger.tagSentence("문장 1개입니다.")
 * List sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * List sentences = tagger.invoke("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * @since 1.x
 */
abstract class CanTagASentence : CanTag {
    final override fun tagSentence(text: String): Sentence {
        val trim = text.trim()
        return if (trim.isNotEmpty()) convertSentence(tagSentenceOriginal(trim))
        else Sentence.empty
    }

    /**
     * 변환되지않은, [text]의 분석결과 [S]를 돌려줍니다.
     *
     * @since 1.x
     * @param text 분석할 String.
     * @return 원본 분석기의 분석 결과 (문장 1개)
     */
    abstract fun tagSentenceOriginal(text: String): S

    /**
     * [S] 타입의 분석결과 [result]를 변환, [Sentence]를 구성합니다.
     *
     * @since 1.x
     * @param result 변환할 분석결과.
     * @return 변환된 [Sentence] 객체
     */
    protected abstract fun convertSentence(result: S): Sentence
}

/**
 * 문단1개, 문장1개가 분석가능한 품사분석기 interface. 원본 분석기는 문장 분석 결과를 [S] 타입으로 돌려줍니다.
 *
 * ## 참고
 * **형태소**는 의미를 가지는 요소로서는 더 이상 분석할 수 없는 가장 작은 말의 단위로 정의됩니다.
 *
 * **형태소 분석**은 문장을 형태소의 단위로 나누는 작업을 의미합니다.
 * 예) '문장을 형태소로 나눠봅시다'의 경우,
 * * 문장/일반명사, -을/조사,
 * * 형태소/일반명사, -로/조사,
 * * 나누-(다)/동사, -어-/어미, 보-(다)/동사, -ㅂ시다/어미
 * 로 대략 나눌 수 있습니다.
 *
 * 아래를 참고해보세요.
 * * [Morpheme] 형태소를 저장하는 클래스입니다.
 * * [POS] 형태소의 분류를 담은 Enum class
 *
 * ## 사용법 예제
 * 분석기 `Tagger`가 `CanTagAParagraph`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * val tagger = Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * val tagger = new Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Java
 * ```java
 * Tagger tagger = new Tagger()
 * Sentence sentence = tagger.tagSentence("문장 1개입니다.")
 * List sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * List sentences = tagger.invoke("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * @since 1.x
 */
abstract class CanTagAParagraph : CanTagASentence() {
    final override fun tag(text: String): List {
        val trim = text.trim()
        return if (trim.isNotEmpty()) tagParagraphOriginal(trim).map { convertSentence(it) }
        else emptyList()
    }

    /**
     * 변환되지않은, [text]의 분석결과 [List]<[S]>를 반환합니다
     *
     * @since 1.x
     * @param text 분석할 String.
     * @return 원본 분석기의 결과인 문장의 목록
     */
    abstract fun tagParagraphOriginal(text: String): List
}

/**
 * 문단1개는 불가하지만, 문장1개가 분석가능한 품사분석기 interface. 원본 분석기는 문장 분석 결과를 [S] 타입으로 돌려줍니다.
 *
 * ## 참고
 * **형태소**는 의미를 가지는 요소로서는 더 이상 분석할 수 없는 가장 작은 말의 단위로 정의됩니다.
 *
 * **형태소 분석**은 문장을 형태소의 단위로 나누는 작업을 의미합니다.
 * 예) '문장을 형태소로 나눠봅시다'의 경우,
 * * 문장/일반명사, -을/조사,
 * * 형태소/일반명사, -로/조사,
 * * 나누-(다)/동사, -어-/어미, 보-(다)/동사, -ㅂ시다/어미
 * 로 대략 나눌 수 있습니다.
 *
 * 아래를 참고해보세요.
 * * [Morpheme] 형태소를 저장하는 클래스입니다.
 * * [POS] 형태소의 분류를 담은 Enum class
 *
 * ## 사용법 예제
 * 분석기 `Tagger`가 `CanTagOnlyASentence`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * val tagger = Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * val tagger = new Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Java
 * ```java
 * Tagger tagger = new Tagger()
 * Sentence sentence = tagger.tagSentence("문장 1개입니다.")
 * List sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * List sentences = tagger.invoke("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * @since 1.x
 */
abstract class CanTagOnlyASentence : CanTag {
    final override fun tagSentence(text: String): Sentence {
        val trim = text.trim()
        return if (trim.isNotEmpty()) convertSentence(trim, tagSentenceOriginal(trim))
        else Sentence.empty
    }

    final override fun tag(text: String): List {
        val trim = text.trim()
        return if (trim.isNotEmpty()) SentenceSplitter(convertSentence(trim, tagSentenceOriginal(trim)))
        else emptyList()
    }

    /**
     * 변환되지않은, [text]의 분석결과 [S]를 반환합니다.
     *
     * @since 1.x
     * @param text 분석할 String.
     * @return 원본 분석기의 결과인 문장 1개
     */
    abstract fun tagSentenceOriginal(text: String): S

    /**
     * [S] 타입의 분석결과 [result]를 변환, [Sentence]를 구성합니다.
     *
     * @since 1.x
     * @param text 품사분석을 수행한 문단의 String입니다.
     * @param result 변환할 분석결과.
     * @return 변환된 [Sentence] 객체
     */
    protected abstract fun convertSentence(text: String, result: S): Sentence
}

/**
 * 문장1개는 불가하지만, 문단1개가 분석가능한 품사분석기 interface. 원본 분석기는 문장 분석 결과를 [S] 타입으로 돌려줍니다.
 *
 * ## 참고
 * **형태소**는 의미를 가지는 요소로서는 더 이상 분석할 수 없는 가장 작은 말의 단위로 정의됩니다.
 *
 * **형태소 분석**은 문장을 형태소의 단위로 나누는 작업을 의미합니다.
 * 예) '문장을 형태소로 나눠봅시다'의 경우,
 * * 문장/일반명사, -을/조사,
 * * 형태소/일반명사, -로/조사,
 * * 나누-(다)/동사, -어-/어미, 보-(다)/동사, -ㅂ시다/어미
 * 로 대략 나눌 수 있습니다.
 *
 * 아래를 참고해보세요.
 * * [Morpheme] 형태소를 저장하는 클래스입니다.
 * * [POS] 형태소의 분류를 담은 Enum class
 *
 * ## 사용법 예제
 * 분석기 `Tagger`가 `CanTagOnlyAParagraph`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * val tagger = Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * val tagger = new Tagger()
 * val sentence = tagger.tagSentence("문장 1개입니다.")
 * val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * ### Java
 * ```java
 * Tagger tagger = new Tagger()
 * Sentence sentence = tagger.tagSentence("문장 1개입니다.")
 * List sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
 * // 또는
 * List sentences = tagger.invoke("문장들입니다. 결과는 목록이 됩니다.")
 * ```
 *
 * @since 1.x
 */
abstract class CanTagOnlyAParagraph : CanTag {
    final override fun tagSentence(text: String): Sentence {
        val trim = text.trim()
        return if (trim.isNotEmpty()) Sentence(tag(trim).flatten())
        else Sentence.empty
    }

    final override fun tag(text: String): List {
        val trim = text.trim()
        return if (trim.isNotEmpty()) tagParagraphOriginal(trim).map { convertSentence(it) }
        else emptyList()
    }

    /**
     * 변환되지않은, [text]의 분석결과 [List]<[S]>를 반환합니다.
     *
     * @since 1.x
     * @param text 분석할 String.
     * @return 원본 분석기의 결과인 문장의 목록입니다.
     */
    abstract fun tagParagraphOriginal(text: String): List

    /**
     * [S] 타입의 분석결과 [result]를 변환, [Sentence]를 구성합니다.
     *
     * @since 1.x
     * @param result 변환할 분석결과.
     * @return 변환된 [Sentence] 객체
     */
    protected abstract fun convertSentence(result: S): Sentence
}

/**
 * [Sentence] 객체에 property를 추가할 수 있는 interface입니다.
 * [INTERMEDIATE]는 각 분석기에서 문장을 분석한 결과물의 중간 형태, 즉 분석기의 입력 형태입니다.
 *
 * 다음 분석의 기본 틀로 사용됩니다.
 * - 구문구조 분석 [CanParseSyntax]
 * - 의존구문구조 분석 [CanParseDependency]
 * - 의미역 분석 [CanLabelSemanticRole]
 * - 개체명 인식 [CanRecognizeEntity]
 * - 다의어/동형이의어 분별 [CanDisambiguateSense]
 *
 * @since 2.0.0
 */
interface CanAnalyzeProperty {
    /**
     * [item]을 분석하여 property 값을 반환합니다.
     *
     * @since 2.0.0
     * @param item 분석 단위 1개입니다.
     * @param sentence 원본 문장입니다.
     * @return 분석의 결과물입니다.
     */
    fun attachProperty(item: INTERMEDIATE, sentence: String): Sentence

    /**
     * String [sentence]를 품사 분석하여 분석기가 받아들이는 [List]>으로 변환합니다.
     *
     * @since 2.0.0
     * @param sentence 텍스트에서 변환할 문장입니다.
     * @return 분석기가 받아들일 수 있는 형태의 데이터입니다. 각 Pair의 [Pair.first] 값은 변환된 문장 객체이며, [Pair.second] 값은 해당 문장의 String 값입니다.
     */
    fun convert(sentence: String): List>

    /**
     * Sentence [sentence]를 해체하여 분석기가 받아들이는 [INTERMEDIATE]로 변환합니다.
     *
     * @since 2.0.0
     * @param sentence 변환할 문장입니다.
     * @return 분석기가 받아들일 수 있는 형태의 데이터입니다.
     */
    fun convert(sentence: Sentence): INTERMEDIATE

    /**
     * 분석기의 중간 결과인 [sentence]를 조합하여 [Sentence] 객체로 변환합니다.
     *
     * @since 2.0.0
     * @param sentence 변환할 문장입니다.
     * @return [Sentence] 객체입니다.
     */
    fun convert(sentence: INTERMEDIATE): Sentence

    /**
     * String [sentence]를 분석함. 결과는 각 [Sentence]의 property로 저장합니다.
     *
     * @since 2.0.0
     * @param sentence 텍스트에서 변환할 문장입니다.
     * @return 결과가 부착된 문장입니다.
     */
    fun analyze(sentence: String): List {
        val paragraph = convert(sentence)
        return paragraph.map { attachProperty(it.first, it.second) }
    }

    /**
     * [sentence]를 분석함. 결과는 각 [Sentence]의 property로 저장됨.
     *
     * @since 2.0.0
     * @param sentence 분석 결과를 부착할 문장입니다.
     * @return 결과가 부착된 문장입니다.
     */
    fun analyze(sentence: Sentence): Sentence {
        return attachProperty(convert(sentence), sentence.surfaceString())
    }

    /**
     * [sentences]를 분석함. 결과는 각 [Sentence]의 property로 저장됨.
     *
     * @since 2.0.0
     * @param sentences 분석 결과를 부착할 문장들의 목록입니다.
     * @return 결과가 부착된 문장들의 목록입니다.
     */
    fun analyze(sentences: List): List = sentences.map { analyze(it) }

    /**
     * [sentence]를 분석함. 결과는 각 [Sentence]의 property로 저장됨.
     *
     * @since 2.0.0
     * @param sentence 텍스트에서 변환할 문장입니다.
     * @return 결과가 부착된 문장입니다.
     */
    operator fun invoke(sentence: String) = analyze(sentence)

    /**
     * [sentence]를 분석함. 결과는 각 [Sentence]의 property로 저장됨.
     *
     * @since 2.0.0
     * @param sentence 분석 결과를 부착할 문장입니다.
     * @return 결과가 부착된 문장입니다.
     */
    operator fun invoke(sentence: Sentence) = analyze(sentence)

    /**
     * [sentences]를 분석함. 결과는 각 [Sentence]의 property로 저장됨.
     *
     * @since 2.0.0
     * @param sentences 분석 결과를 부착할 문장들의 목록입니다.
     * @return 결과가 부착된 문장들의 목록입니다.
     */
    operator fun invoke(sentences: List) = analyze(sentences)
}


/**
 * 구문분석을 수행하는 Interface입니다.
 * 매개변수 [T]는 각 분석기에서 문장을 분석한 결과물의 중간 형태, 즉 분석기의 입력 형태입니다.
 *
 * ## 참고
 * **구문구조 분석**은 문장의 구성요소들(어절, 구, 절)이 이루는 문법적 구조를 분석하는 방법입니다.
 * 예) '나는 밥을 먹었고, 영희는 짐을 쌌다'라는 문장에는
 * 2개의 절이 있습니다
 * * 나는 밥을 먹었고
 * * 영희는 짐을 쌌다
 * 각 절은 3개의 구를 포함합니다
 * * 나는, 밥을, 영희는, 짐을: 체언구
 * * 먹었고, 쌌다: 용언구
 *
 * 아래를 참고해보세요.
 * * [Word.getPhrase] 어절이 직접 속하는 가장 작은 구구조 [SyntaxTree]를 가져오는 API
 * * [Sentence.getSyntaxTree] 전체 문장을 분석한 [SyntaxTree]를 가져오는 API
 * * [SyntaxTree] 구구조를 저장하는 형태
 * * [PhraseTag] 구구조의 형태 분류를 갖는 Enum 값
 *
 * ## 사용법 예제
 * 분석기 `Parser`가 `CanParseSyntax`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * // 문장에서 바로 분석할 때
 * val parser = Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: List = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * // 문장에서 바로 분석할 때
 * val parser = new Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: java.util.List[Sentence] = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Java
 * ```java
 * // 문장에서 바로 분석할 때
 * Parser parser = Parser()
 * List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * Sentence taggedSentence = ...
 * Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
 *
 * List taggedSentList = ...
 * List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
 * ```
 *
 * @since 2.0.0
 */
interface CanParseSyntax : CanAnalyzeProperty

/**
 * 의존구문분석을 수행하는 Interface입니다.
 * 매개변수 [T]는 각 분석기에서 문장을 분석한 결과물의 중간 형태, 즉 분석기의 입력 형태입니다.
 *
 * ## 참고
 * **의존구조 분석**은 문장의 구성 어절들이 의존 또는 기능하는 관계를 분석하는 방법입니다.
 * 예) '나는 밥을 먹었고, 영희는 짐을 쌌다'라는 문장에는
 * 가장 마지막 단어인 '쌌다'가 핵심 어구가 되며,
 * * '먹었고'가 '쌌다'와 대등하게 연결되고
 * * '나는'은 '먹었고'의 주어로 기능하며
 * * '밥을'은 '먹었고'의 목적어로 기능합니다.
 * * '영희는'은 '쌌다'의 주어로 기능하고,
 * * '짐을'은 '쌌다'의 목적어로 기능합니다.
 *
 * 아래를 참고해보세요.
 * * [Word.getDependentEdges] 어절이 직접 지배하는 하위 의존구조 [DepEdge]의 목록을 가져오는 API
 * * [Word.getGovernorEdge] 어절이 지배당하는 상위 의존구조 [DepEdge]를 가져오는 API
 * * [Sentence.getDependencies] 전체 문장을 분석한 의존구조 [DepEdge]의 목록을 가져오는 API
 * * [DepEdge] 의존구조를 저장하는 형태
 * * [PhraseTag] 의존구조의 형태 분류를 갖는 Enum 값 (구구조 분류와 같음)
 * * [DependencyTag] 의존구조의 기능 분류를 갖는 Enum 값
 *
 * ## 사용법 예제
 * 분석기 `Parser`가 `CanParseDependency`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * // 문장에서 바로 분석할 때
 * val parser = Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: List = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * // 문장에서 바로 분석할 때
 * val parser = new Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: java.util.List[Sentence] = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Java
 * ```java
 * // 문장에서 바로 분석할 때
 * Parser parser = Parser()
 * List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * Sentence taggedSentence = ...
 * Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
 *
 * List taggedSentList = ...
 * List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
 * ```
 *
 * @since 2.0.0
 */
interface CanParseDependency : CanAnalyzeProperty

/**
 * 의미역 분석(Semantic Role Labeling)을 수행하는 Interface입니다.
 * 매개변수 [T]는 각 분석기에서 문장을 분석한 결과물의 중간 형태, 즉 분석기의 입력 형태입니다.
 *
 * ## 참고
 * **의미역 결정**은 문장의 구성 어절들의 역할/기능을 분석하는 방법입니다.
 * 예) '나는 밥을 어제 집에서 먹었다'라는 문장에는
 * 동사 '먹었다'를 중심으로
 * * '나는'은 동작의 주체를,
 * * '밥을'은 동작의 대상을,
 * * '어제'는 동작의 시점을
 * * '집에서'는 동작의 장소를 나타냅니다.
 *
 * 아래를 참고해보세요.
 * * [Word.getArgumentRoles] 어절이 술어인 논항들의 [RoleEdge] 목록을 가져오는 API
 * * [Word.getPredicateRole] 어절이 논항인 [RoleEdge]의 술어를 가져오는 API
 * * [Sentence.getRoles] 전체 문장을 분석한 의미역 구조 [RoleEdge]를 가져오는 API
 * * [RoleEdge] 의미역 구조를 저장하는 형태
 * * [RoleType] 의미역 분류를 갖는 Enum 값
 *
 * ## 사용법 예제
 * 분석기 `Parser`가 `CanLabelSemanticRole`을 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * // 문장에서 바로 분석할 때
 * val parser = Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: List = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * // 문장에서 바로 분석할 때
 * val parser = new Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: java.util.List[Sentence] = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Java
 * ```java
 * // 문장에서 바로 분석할 때
 * Parser parser = Parser()
 * List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * Sentence taggedSentence = ...
 * Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
 *
 * List taggedSentList = ...
 * List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
 * ```
 *
 * @since 2.0.0
 */
interface CanLabelSemanticRole : CanAnalyzeProperty

/**
 * 개체명 인식 (Named Entity Recognition)을 수행하는 Interface입니다.
 * 매개변수 [T]는 각 분석기에서 문장을 분석한 결과물의 중간 형태, 즉 분석기의 입력 형태입니다.
 *
 * ## 참고
 * **개체명 인식**은 문장에서 인물, 장소, 기관, 대상 등을 인식하는 기술입니다.
 * 예) '철저한 진상 조사를 촉구하는 국제사회의 목소리가 커지고 있는 가운데, 트럼프 미국 대통령은 되레 사우디를 감싸고 나섰습니다.'에서, 다음을 인식하는 기술입니다.
 * * '트럼프': 인물
 * * '미국' : 국가
 * * '대통령' : 직위
 * * '사우디' : 국가
 *
 * 아래를 참고해보세요.
 * * [Word.getEntities] 어절이 속하는 [Entity]를 가져오는 API
 * * [Sentence.getEntities] 문장에 포함된 모든 [Entity]를 가져오는 API
 * * [Entity] 개체명을 저장하는 형태
 * * [CoarseEntityType] [Entity]의 대분류 개체명 분류구조 Enum 값
 *
 * ## 사용법 예제
 * 분석기 `Parser`가 `CanRecognizeEntity`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * // 문장에서 바로 분석할 때
 * val parser = Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: List = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * // 문장에서 바로 분석할 때
 * val parser = new Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: java.util.List[Sentence] = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Java
 * ```java
 * // 문장에서 바로 분석할 때
 * Parser parser = Parser()
 * List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * Sentence taggedSentence = ...
 * Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
 *
 * List taggedSentList = ...
 * List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
 * ```
 *
 * @since 2.0.0
 */
interface CanRecognizeEntity : CanAnalyzeProperty

/**
 * 다의어 분별 (Word sense disambiguation)을 수행하는 Interface입니다.
 * 매개변수 [T]는 각 분석기에서 문장을 분석한 결과물의 중간 형태, 즉 분석기의 입력 형태입니다.
 *
 * ## 참고
 * **다의어 분별**은 동일한 단어의 여러 의미를 구분하는 작업입니다.
 * 예) '말1'은 다음 의미를 갖는 다의어이며, 다의어 분별 작업은 이를 구분합니다.
 * 1. 사람의 생각이나 느낌 따위를 표현하고 전달하는 데 쓰는 음성 기호.
 * 2. 음성 기호로 생각이나 느낌을 표현하고 전달하는 행위. 또는 그런 결과물.
 * 3. 일정한 주제나 줄거리를 가진 이야기.
 *
 * **동형이의어 분별**은 동일한 형태지만 다른 의미를 갖는 어절을 구분하는 작업입니다.
 * 예) '말'은 다음과 같은 여러 동형이의어의 표면형입니다.
 * 1. '말1': 사람의 생각이나 느낌 따위를 표현하고 전달하는 데 쓰는 음성 기호
 * 2. '말2': 톱질을 하거나 먹줄을 그을 때 밑에 받치는 나무
 * 3. '말3': 곡식, 액체, 가루 따위의 분량을 되는 데 쓰는 그릇
 * 4. '말4': 말과의 포유류
 * ...
 *
 * 아래를 참고해보세요.
 * * [WordSense] 동형이의어/다의어의 의미번호를 저장하는 형태
 * * [Morpheme.getWordSense] 형태소의 어깨번호/의미번호를 가져오는 API
 *
 * ## 사용법 예제
 * 분석기 `Parser`가 `CanDisambiguateSense`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * // 문장에서 바로 분석할 때
 * val parser = Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: List = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * // 문장에서 바로 분석할 때
 * val parser = new Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: java.util.List[Sentence] = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Java
 * ```java
 * // 문장에서 바로 분석할 때
 * Parser parser = Parser()
 * List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * Sentence taggedSentence = ...
 * Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
 *
 * List taggedSentList = ...
 * List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
 * ```
 *
 * @since 2.0.0
 */
interface CanDisambiguateSense : CanAnalyzeProperty


/**
 * 상호참조 해소, 공지시어 해소 (Coreference Resolution) 또는 대용어 분석 (Anaphora Resolution)을 수행하는 Interface입니다.
 *
 * ## 참고
 * **공지시어 해소**는 문장 내 또는 문장 간에 같은 대상을 지칭하는 어구를 찾아 묶는 분석과정입니다.
 * 예) '삼성그룹의 계열사인 삼성물산은 같은 그룹의 계열사인 삼성생명과 함께'라는 문장에서
 * * '삼성그룹'과 '같은 그룹'을 찾아 묶는 것을 말합니다.
 *
 * **영형대용어 분석**은 문장에서 생략된 기능어를 찾아 문장 내 또는 문장 간에 언급되어 있는 어구와 묶는 분석과정입니다.
 * 예) '나는 밥을 먹었고, 영희도 먹었다'라는 문장에서,
 * * '먹었다'의 목적어인 '밥을'이 생략되어 있음을 찾는 것을 말합니다.
 *
 * 아래를 참고해보세요.
 * * [Sentence.getCorefGroups] 문장 내에 포함된 개체명 묶음 [CoreferenceGroup]들의 목록을 반환하는 API
 * * [Entity.getCorefGroup] 각 개체명이 속하는 [CoreferenceGroup]을 가져오는 API
 * * [CoreferenceGroup] 동일한 대상을 지칭하는 개체명을 묶는 API
 *
 * ## 사용법 예제
 * 분석기 `Parser`가 `CanResolveCoref`를 상속받았다면,
 *
 * ### Kotlin
 * ```kotlin
 * // 문장에서 바로 분석할 때
 * val parser = Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: List = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
 * ```scala
 * import kr.bydelta.koala.Implicits._
 * // 문장에서 바로 분석할 때
 * val parser = new Parser()
 * val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * val taggedSentence: Sentence = ...
 * val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
 *
 * val taggedSentList: java.util.List[Sentence] = ...
 * val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
 * ```
 *
 * ### Java
 * ```java
 * // 문장에서 바로 분석할 때
 * Parser parser = Parser()
 * List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
 *
 * // 타 분석기에서 분석한 다음 이어서 분석할 때
 * Sentence taggedSentence = ...
 * Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
 *
 * List taggedSentList = ...
 * List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
 * ```
 *
 * @since 2.0.0
 */
interface CanResolveCoref : CanAnalyzeProperty